import PropTypes from 'prop-types'; import { PureComponent } from 'react'; import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; import { Helmet } from 'react-helmet'; import { List as ImmutableList } from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { submitSearch, expandSearch } from 'flavours/glitch/actions/search'; import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; import { Icon } from 'flavours/glitch/components/icon'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import Account from 'flavours/glitch/containers/account_container'; import Status from 'flavours/glitch/containers/status_container'; import { SearchSection } from './components/search_section'; const messages = defineMessages({ title: { id: 'search_results.title', defaultMessage: 'Search for {q}' }, }); const mapStateToProps = state => ({ isLoading: state.getIn(['search', 'isLoading']), results: state.getIn(['search', 'results']), q: state.getIn(['search', 'searchTerm']), submittedType: state.getIn(['search', 'type']), }); const INITIAL_PAGE_LIMIT = 10; const INITIAL_DISPLAY = 4; const hidePeek = list => { if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) { return list.skipLast(1); } else { return list; } }; const renderAccounts = accounts => hidePeek(accounts).map(id => ( <Account key={id} id={id} /> )); const renderHashtags = hashtags => hidePeek(hashtags).map(hashtag => ( <Hashtag key={hashtag.get('name')} hashtag={hashtag} /> )); const renderStatuses = statuses => hidePeek(statuses).map(id => ( <Status key={id} id={id} /> )); class Results extends PureComponent { static propTypes = { results: ImmutablePropTypes.contains({ accounts: ImmutablePropTypes.orderedSet, statuses: ImmutablePropTypes.orderedSet, hashtags: ImmutablePropTypes.orderedSet, }), isLoading: PropTypes.bool, multiColumn: PropTypes.bool, dispatch: PropTypes.func.isRequired, q: PropTypes.string, intl: PropTypes.object, submittedType: PropTypes.oneOf(['accounts', 'statuses', 'hashtags']), }; state = { type: this.props.submittedType || 'all', }; static getDerivedStateFromProps(props, state) { if (props.submittedType !== state.type) { return { type: props.submittedType || 'all', }; } return null; } handleSelectAll = () => { const { submittedType, dispatch } = this.props; // If we originally searched for a specific type, we need to resubmit // the query to get all types of results if (submittedType) { dispatch(submitSearch()); } this.setState({ type: 'all' }); }; handleSelectAccounts = () => { const { submittedType, dispatch } = this.props; // If we originally searched for something else (but not everything), // we need to resubmit the query for this specific type if (submittedType !== 'accounts') { dispatch(submitSearch('accounts')); } this.setState({ type: 'accounts' }); }; handleSelectHashtags = () => { const { submittedType, dispatch } = this.props; // If we originally searched for something else (but not everything), // we need to resubmit the query for this specific type if (submittedType !== 'hashtags') { dispatch(submitSearch('hashtags')); } this.setState({ type: 'hashtags' }); }; handleSelectStatuses = () => { const { submittedType, dispatch } = this.props; // If we originally searched for something else (but not everything), // we need to resubmit the query for this specific type if (submittedType !== 'statuses') { dispatch(submitSearch('statuses')); } this.setState({ type: 'statuses' }); }; handleLoadMoreAccounts = () => this._loadMore('accounts'); handleLoadMoreStatuses = () => this._loadMore('statuses'); handleLoadMoreHashtags = () => this._loadMore('hashtags'); _loadMore (type) { const { dispatch } = this.props; dispatch(expandSearch(type)); } handleLoadMore = () => { const { type } = this.state; if (type !== 'all') { this._loadMore(type); } }; render () { const { intl, isLoading, q, results } = this.props; const { type } = this.state; // We request 1 more result than we display so we can tell if there'd be a next page const hasMore = type !== 'all' ? results.get(type, ImmutableList()).size > INITIAL_PAGE_LIMIT && results.get(type).size % INITIAL_PAGE_LIMIT === 1 : false; let filteredResults; const accounts = results.get('accounts', ImmutableList()); const hashtags = results.get('hashtags', ImmutableList()); const statuses = results.get('statuses', ImmutableList()); switch(type) { case 'all': filteredResults = (accounts.size + hashtags.size + statuses.size) > 0 ? ( <> {accounts.size > 0 && ( <SearchSection key='accounts' title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>} onClickMore={this.handleLoadMoreAccounts}> {accounts.take(INITIAL_DISPLAY).map(id => <Account key={id} id={id} />)} </SearchSection> )} {hashtags.size > 0 && ( <SearchSection key='hashtags' title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>} onClickMore={this.handleLoadMoreHashtags}> {hashtags.take(INITIAL_DISPLAY).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} </SearchSection> )} {statuses.size > 0 && ( <SearchSection key='statuses' title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>} onClickMore={this.handleLoadMoreStatuses}> {statuses.take(INITIAL_DISPLAY).map(id => <Status key={id} id={id} />)} </SearchSection> )} </> ) : []; break; case 'accounts': filteredResults = renderAccounts(accounts); break; case 'hashtags': filteredResults = renderHashtags(hashtags); break; case 'statuses': filteredResults = renderStatuses(statuses); break; } return ( <> <div className='account__section-headline'> <button onClick={this.handleSelectAll} className={type === 'all' ? 'active' : undefined}><FormattedMessage id='search_results.all' defaultMessage='All' /></button> <button onClick={this.handleSelectAccounts} className={type === 'accounts' ? 'active' : undefined}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button> <button onClick={this.handleSelectHashtags} className={type === 'hashtags' ? 'active' : undefined}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button> <button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button> </div> <div className='explore__search-results'> <ScrollableList scrollKey='search-results' isLoading={isLoading} onLoadMore={this.handleLoadMore} hasMore={hasMore} emptyMessage={<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />} bindToDocument > {filteredResults} </ScrollableList> </div> <Helmet> <title>{intl.formatMessage(messages.title, { q })}</title> </Helmet> </> ); } } export default connect(mapStateToProps)(injectIntl(Results));