Add infinite scrolling for search results in web UI (#26784)
This commit is contained in:
		
					parent
					
						
							
								548c032dbb
							
						
					
				
			
			
				commit
				
					
						5d20733d8d
					
				
			
		
					 7 changed files with 255 additions and 171 deletions
				
			
		|  | @ -37,17 +37,17 @@ export function submitSearch(type) { | ||||||
|     const signedIn = !!getState().getIn(['meta', 'me']); |     const signedIn = !!getState().getIn(['meta', 'me']); | ||||||
| 
 | 
 | ||||||
|     if (value.length === 0) { |     if (value.length === 0) { | ||||||
|       dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '')); |       dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type)); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     dispatch(fetchSearchRequest()); |     dispatch(fetchSearchRequest(type)); | ||||||
| 
 | 
 | ||||||
|     api(getState).get('/api/v2/search', { |     api(getState).get('/api/v2/search', { | ||||||
|       params: { |       params: { | ||||||
|         q: value, |         q: value, | ||||||
|         resolve: signedIn, |         resolve: signedIn, | ||||||
|         limit: 5, |         limit: 11, | ||||||
|         type, |         type, | ||||||
|       }, |       }, | ||||||
|     }).then(response => { |     }).then(response => { | ||||||
|  | @ -59,7 +59,7 @@ export function submitSearch(type) { | ||||||
|         dispatch(importFetchedStatuses(response.data.statuses)); |         dispatch(importFetchedStatuses(response.data.statuses)); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       dispatch(fetchSearchSuccess(response.data, value)); |       dispatch(fetchSearchSuccess(response.data, value, type)); | ||||||
|       dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); |       dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); | ||||||
|     }).catch(error => { |     }).catch(error => { | ||||||
|       dispatch(fetchSearchFail(error)); |       dispatch(fetchSearchFail(error)); | ||||||
|  | @ -67,16 +67,18 @@ export function submitSearch(type) { | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function fetchSearchRequest() { | export function fetchSearchRequest(searchType) { | ||||||
|   return { |   return { | ||||||
|     type: SEARCH_FETCH_REQUEST, |     type: SEARCH_FETCH_REQUEST, | ||||||
|  |     searchType, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function fetchSearchSuccess(results, searchTerm) { | export function fetchSearchSuccess(results, searchTerm, searchType) { | ||||||
|   return { |   return { | ||||||
|     type: SEARCH_FETCH_SUCCESS, |     type: SEARCH_FETCH_SUCCESS, | ||||||
|     results, |     results, | ||||||
|  |     searchType, | ||||||
|     searchTerm, |     searchTerm, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  | @ -90,15 +92,16 @@ export function fetchSearchFail(error) { | ||||||
| 
 | 
 | ||||||
| export const expandSearch = type => (dispatch, getState) => { | export const expandSearch = type => (dispatch, getState) => { | ||||||
|   const value  = getState().getIn(['search', 'value']); |   const value  = getState().getIn(['search', 'value']); | ||||||
|   const offset = getState().getIn(['search', 'results', type]).size; |   const offset = getState().getIn(['search', 'results', type]).size - 1; | ||||||
| 
 | 
 | ||||||
|   dispatch(expandSearchRequest()); |   dispatch(expandSearchRequest(type)); | ||||||
| 
 | 
 | ||||||
|   api(getState).get('/api/v2/search', { |   api(getState).get('/api/v2/search', { | ||||||
|     params: { |     params: { | ||||||
|       q: value, |       q: value, | ||||||
|       type, |       type, | ||||||
|       offset, |       offset, | ||||||
|  |       limit: 11, | ||||||
|     }, |     }, | ||||||
|   }).then(({ data }) => { |   }).then(({ data }) => { | ||||||
|     if (data.accounts) { |     if (data.accounts) { | ||||||
|  | @ -116,8 +119,9 @@ export const expandSearch = type => (dispatch, getState) => { | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const expandSearchRequest = () => ({ | export const expandSearchRequest = (searchType) => ({ | ||||||
|   type: SEARCH_EXPAND_REQUEST, |   type: SEARCH_EXPAND_REQUEST, | ||||||
|  |   searchType, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export const expandSearchSuccess = (results, searchTerm, searchType) => ({ | export const expandSearchSuccess = (results, searchTerm, searchType) => ({ | ||||||
|  |  | ||||||
|  | @ -1,46 +1,36 @@ | ||||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||||
| 
 | 
 | ||||||
| import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||||
| 
 | 
 | ||||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||||
| 
 | 
 | ||||||
| import { Icon }  from 'mastodon/components/icon'; | import { Icon }  from 'mastodon/components/icon'; | ||||||
| import { LoadMore } from 'mastodon/components/load_more'; | import { LoadMore } from 'mastodon/components/load_more'; | ||||||
|  | import { SearchSection } from 'mastodon/features/explore/components/search_section'; | ||||||
| 
 | 
 | ||||||
| import { ImmutableHashtag as Hashtag } from '../../../components/hashtag'; | import { ImmutableHashtag as Hashtag } from '../../../components/hashtag'; | ||||||
| import AccountContainer from '../../../containers/account_container'; | import AccountContainer from '../../../containers/account_container'; | ||||||
| import StatusContainer from '../../../containers/status_container'; | import StatusContainer from '../../../containers/status_container'; | ||||||
| import { searchEnabled } from '../../../initial_state'; |  | ||||||
| 
 | 
 | ||||||
| const messages = defineMessages({ | const INITIAL_PAGE_LIMIT = 10; | ||||||
|   dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, | 
 | ||||||
| }); | const withoutLastResult = list => { | ||||||
|  |   if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) { | ||||||
|  |     return list.skipLast(1); | ||||||
|  |   } else { | ||||||
|  |     return list; | ||||||
|  |   } | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| class SearchResults extends ImmutablePureComponent { | class SearchResults extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     results: ImmutablePropTypes.map.isRequired, |     results: ImmutablePropTypes.map.isRequired, | ||||||
|     suggestions: ImmutablePropTypes.list.isRequired, |  | ||||||
|     fetchSuggestions: PropTypes.func.isRequired, |  | ||||||
|     expandSearch: PropTypes.func.isRequired, |     expandSearch: PropTypes.func.isRequired, | ||||||
|     dismissSuggestion: PropTypes.func.isRequired, |  | ||||||
|     searchTerm: PropTypes.string, |     searchTerm: PropTypes.string, | ||||||
|     intl: PropTypes.object.isRequired, |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   componentDidMount () { |  | ||||||
|     if (this.props.searchTerm === '') { |  | ||||||
|       this.props.fetchSuggestions(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   componentDidUpdate () { |  | ||||||
|     if (this.props.searchTerm === '') { |  | ||||||
|       this.props.fetchSuggestions(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   handleLoadMoreAccounts = () => this.props.expandSearch('accounts'); |   handleLoadMoreAccounts = () => this.props.expandSearch('accounts'); | ||||||
| 
 | 
 | ||||||
|   handleLoadMoreStatuses = () => this.props.expandSearch('statuses'); |   handleLoadMoreStatuses = () => this.props.expandSearch('statuses'); | ||||||
|  | @ -48,97 +38,52 @@ class SearchResults extends ImmutablePureComponent { | ||||||
|   handleLoadMoreHashtags = () => this.props.expandSearch('hashtags'); |   handleLoadMoreHashtags = () => this.props.expandSearch('hashtags'); | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props; |     const { results } = this.props; | ||||||
| 
 |  | ||||||
|     if (searchTerm === '' && !suggestions.isEmpty()) { |  | ||||||
|       return ( |  | ||||||
|         <div className='search-results'> |  | ||||||
|           <div className='trends'> |  | ||||||
|             <div className='trends__header'> |  | ||||||
|               <Icon id='user-plus' fixedWidth /> |  | ||||||
|               <FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' /> |  | ||||||
|             </div> |  | ||||||
| 
 |  | ||||||
|             {suggestions && suggestions.map(suggestion => ( |  | ||||||
|               <AccountContainer |  | ||||||
|                 key={suggestion.get('account')} |  | ||||||
|                 id={suggestion.get('account')} |  | ||||||
|                 actionIcon={suggestion.get('source') === 'past_interactions' ? 'times' : null} |  | ||||||
|                 actionTitle={suggestion.get('source') === 'past_interactions' ? intl.formatMessage(messages.dismissSuggestion) : null} |  | ||||||
|                 onActionClick={dismissSuggestion} |  | ||||||
|               /> |  | ||||||
|             ))} |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     let accounts, statuses, hashtags; |     let accounts, statuses, hashtags; | ||||||
|     let count = 0; |  | ||||||
| 
 | 
 | ||||||
|     if (results.get('accounts') && results.get('accounts').size > 0) { |     if (results.get('accounts') && results.get('accounts').size > 0) { | ||||||
|       count   += results.get('accounts').size; |  | ||||||
|       accounts = ( |       accounts = ( | ||||||
|         <div className='search-results__section'> |         <SearchSection title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}> | ||||||
|           <h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></h5> |           {withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)} | ||||||
| 
 |           {(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreAccounts} />} | ||||||
|           {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} |         </SearchSection> | ||||||
| 
 |  | ||||||
|           {results.get('accounts').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreAccounts} />} |  | ||||||
|         </div> |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (results.get('statuses') && results.get('statuses').size > 0) { |  | ||||||
|       count   += results.get('statuses').size; |  | ||||||
|       statuses = ( |  | ||||||
|         <div className='search-results__section'> |  | ||||||
|           <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5> |  | ||||||
| 
 |  | ||||||
|           {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} |  | ||||||
| 
 |  | ||||||
|           {results.get('statuses').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreStatuses} />} |  | ||||||
|         </div> |  | ||||||
|       ); |  | ||||||
|     } else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) { |  | ||||||
|       statuses = ( |  | ||||||
|         <div className='search-results__section'> |  | ||||||
|           <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5> |  | ||||||
| 
 |  | ||||||
|           <div className='search-results__info'> |  | ||||||
|             <FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching posts by their content is not enabled on this Mastodon server.' /> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (results.get('hashtags') && results.get('hashtags').size > 0) { |     if (results.get('hashtags') && results.get('hashtags').size > 0) { | ||||||
|       count += results.get('hashtags').size; |  | ||||||
|       hashtags = ( |       hashtags = ( | ||||||
|         <div className='search-results__section'> |         <SearchSection title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}> | ||||||
|           <h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> |           {withoutLastResult(results.get('hashtags')).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} | ||||||
| 
 |           {(results.get('hashtags').size > INITIAL_PAGE_LIMIT && results.get('hashtags').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreHashtags} />} | ||||||
|           {results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} |         </SearchSection> | ||||||
| 
 |  | ||||||
|           {results.get('hashtags').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreHashtags} />} |  | ||||||
|         </div> |  | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (results.get('statuses') && results.get('statuses').size > 0) { | ||||||
|  |       statuses = ( | ||||||
|  |         <SearchSection title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>}> | ||||||
|  |           {withoutLastResult(results.get('statuses')).map(statusId => <StatusContainer key={statusId} id={statusId} />)} | ||||||
|  |           {(results.get('statuses').size > INITIAL_PAGE_LIMIT && results.get('statuses').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreStatuses} />} | ||||||
|  |         </SearchSection> | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div className='search-results'> |       <div className='search-results'> | ||||||
|         <div className='search-results__header'> |         <div className='search-results__header'> | ||||||
|           <Icon id='search' fixedWidth /> |           <Icon id='search' fixedWidth /> | ||||||
|           <FormattedMessage id='search_results.total' defaultMessage='{count, plural, one {# result} other {# results}}' values={{ count }} /> |           <FormattedMessage id='explore.search_results' defaultMessage='Search results' /> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         {accounts} |         {accounts} | ||||||
|         {statuses} |  | ||||||
|         {hashtags} |         {hashtags} | ||||||
|  |         {statuses} | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default injectIntl(SearchResults); | export default SearchResults; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,20 @@ | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | 
 | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
|  | 
 | ||||||
|  | export const SearchSection = ({ title, onClickMore, children }) => ( | ||||||
|  |   <div className='search-results__section'> | ||||||
|  |     <div className='search-results__section__header'> | ||||||
|  |       <h3>{title}</h3> | ||||||
|  |       {onClickMore && <button onClick={onClickMore}><FormattedMessage id='search_results.see_all' defaultMessage='See all' /></button>} | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     {children} | ||||||
|  |   </div> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | SearchSection.propTypes = { | ||||||
|  |   title: PropTypes.node.isRequired, | ||||||
|  |   onClickMore: PropTypes.func, | ||||||
|  |   children: PropTypes.children, | ||||||
|  | }; | ||||||
|  | @ -9,13 +9,15 @@ import { List as ImmutableList } from 'immutable'; | ||||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||||
| import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
| 
 | 
 | ||||||
| import { expandSearch } from 'mastodon/actions/search'; | import { submitSearch, expandSearch } from 'mastodon/actions/search'; | ||||||
| import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; | import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; | ||||||
| import { LoadMore } from 'mastodon/components/load_more'; | import { Icon } from 'mastodon/components/icon'; | ||||||
| import { LoadingIndicator } from 'mastodon/components/loading_indicator'; | import ScrollableList from 'mastodon/components/scrollable_list'; | ||||||
| import Account from 'mastodon/containers/account_container'; | import Account from 'mastodon/containers/account_container'; | ||||||
| import Status from 'mastodon/containers/status_container'; | import Status from 'mastodon/containers/status_container'; | ||||||
| 
 | 
 | ||||||
|  | import { SearchSection } from './components/search_section'; | ||||||
|  | 
 | ||||||
| const messages = defineMessages({ | const messages = defineMessages({ | ||||||
|   title: { id: 'search_results.title', defaultMessage: 'Search for {q}' }, |   title: { id: 'search_results.title', defaultMessage: 'Search for {q}' }, | ||||||
| }); | }); | ||||||
|  | @ -24,85 +26,175 @@ const mapStateToProps = state => ({ | ||||||
|   isLoading: state.getIn(['search', 'isLoading']), |   isLoading: state.getIn(['search', 'isLoading']), | ||||||
|   results: state.getIn(['search', 'results']), |   results: state.getIn(['search', 'results']), | ||||||
|   q: state.getIn(['search', 'searchTerm']), |   q: state.getIn(['search', 'searchTerm']), | ||||||
|  |   submittedType: state.getIn(['search', 'type']), | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const appendLoadMore = (id, list, onLoadMore) => { | const INITIAL_PAGE_LIMIT = 10; | ||||||
|   if (list.size >= 5) { | const INITIAL_DISPLAY = 4; | ||||||
|     return list.push(<LoadMore key={`${id}-load-more`} visible onClick={onLoadMore} />); | 
 | ||||||
|  | const hidePeek = list => { | ||||||
|  |   if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) { | ||||||
|  |     return list.skipLast(1); | ||||||
|   } else { |   } else { | ||||||
|     return list; |     return list; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const renderAccounts = (results, onLoadMore) => appendLoadMore('accounts', results.get('accounts', ImmutableList()).map(item => ( | const renderAccounts = accounts => hidePeek(accounts).map(id => ( | ||||||
|   <Account key={`account-${item}`} id={item} /> |   <Account key={id} id={id} /> | ||||||
| )), onLoadMore); | )); | ||||||
| 
 | 
 | ||||||
| const renderHashtags = (results, onLoadMore) => appendLoadMore('hashtags', results.get('hashtags', ImmutableList()).map(item => ( | const renderHashtags = hashtags => hidePeek(hashtags).map(hashtag => ( | ||||||
|   <Hashtag key={`tag-${item.get('name')}`} hashtag={item} /> |   <Hashtag key={hashtag.get('name')} hashtag={hashtag} /> | ||||||
| )), onLoadMore); | )); | ||||||
| 
 | 
 | ||||||
| const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', results.get('statuses', ImmutableList()).map(item => ( | const renderStatuses = statuses => hidePeek(statuses).map(id => ( | ||||||
|   <Status key={`status-${item}`} id={item} /> |   <Status key={id} id={id} /> | ||||||
| )), onLoadMore); | )); | ||||||
| 
 | 
 | ||||||
| class Results extends PureComponent { | class Results extends PureComponent { | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     results: ImmutablePropTypes.map, |     results: ImmutablePropTypes.contains({ | ||||||
|  |       accounts: ImmutablePropTypes.orderedSet, | ||||||
|  |       statuses: ImmutablePropTypes.orderedSet, | ||||||
|  |       hashtags: ImmutablePropTypes.orderedSet, | ||||||
|  |     }), | ||||||
|     isLoading: PropTypes.bool, |     isLoading: PropTypes.bool, | ||||||
|     multiColumn: PropTypes.bool, |     multiColumn: PropTypes.bool, | ||||||
|     dispatch: PropTypes.func.isRequired, |     dispatch: PropTypes.func.isRequired, | ||||||
|     q: PropTypes.string, |     q: PropTypes.string, | ||||||
|     intl: PropTypes.object, |     intl: PropTypes.object, | ||||||
|  |     submittedType: PropTypes.oneOf(['accounts', 'statuses', 'hashtags']), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   state = { |   state = { | ||||||
|     type: 'all', |     type: this.props.submittedType || 'all', | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   handleSelectAll = () => this.setState({ type: 'all' }); |   static getDerivedStateFromProps(props, state) { | ||||||
|   handleSelectAccounts = () => this.setState({ type: 'accounts' }); |     if (props.submittedType !== state.type) { | ||||||
|   handleSelectHashtags = () => this.setState({ type: 'hashtags' }); |       return { | ||||||
|   handleSelectStatuses = () => this.setState({ type: 'statuses' }); |         type: props.submittedType || 'all', | ||||||
|   handleLoadMoreAccounts = () => this.loadMore('accounts'); |       }; | ||||||
|   handleLoadMoreStatuses = () => this.loadMore('statuses'); |     } | ||||||
|   handleLoadMoreHashtags = () => this.loadMore('hashtags'); |  | ||||||
| 
 | 
 | ||||||
|   loadMore (type) { |     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; |     const { dispatch } = this.props; | ||||||
|     dispatch(expandSearch(type)); |     dispatch(expandSearch(type)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   handleLoadMore = () => { | ||||||
|  |     const { type } = this.state; | ||||||
|  | 
 | ||||||
|  |     if (type !== 'all') { | ||||||
|  |       this._loadMore(type); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { intl, isLoading, q, results } = this.props; |     const { intl, isLoading, q, results } = this.props; | ||||||
|     const { type } = this.state; |     const { type } = this.state; | ||||||
| 
 | 
 | ||||||
|     let filteredResults = ImmutableList(); |     // 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; | ||||||
| 
 | 
 | ||||||
|     if (!isLoading) { |     if (!isLoading) { | ||||||
|  |       const accounts = results.get('accounts', ImmutableList()); | ||||||
|  |       const hashtags = results.get('hashtags', ImmutableList()); | ||||||
|  |       const statuses = results.get('statuses', ImmutableList()); | ||||||
|  | 
 | ||||||
|       switch(type) { |       switch(type) { | ||||||
|       case 'all': |       case 'all': | ||||||
|         filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts), renderHashtags(results, this.handleLoadMoreHashtags), renderStatuses(results, this.handleLoadMoreStatuses)); |         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; |         break; | ||||||
|       case 'accounts': |       case 'accounts': | ||||||
|         filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts)); |         filteredResults = renderAccounts(accounts); | ||||||
|         break; |         break; | ||||||
|       case 'hashtags': |       case 'hashtags': | ||||||
|         filteredResults = filteredResults.concat(renderHashtags(results, this.handleLoadMoreHashtags)); |         filteredResults = renderHashtags(hashtags); | ||||||
|         break; |         break; | ||||||
|       case 'statuses': |       case 'statuses': | ||||||
|         filteredResults = filteredResults.concat(renderStatuses(results, this.handleLoadMoreStatuses)); |         filteredResults = renderStatuses(statuses); | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
| 
 |  | ||||||
|       if (filteredResults.size === 0) { |  | ||||||
|         filteredResults = ( |  | ||||||
|           <div className='empty-column-indicator'> |  | ||||||
|             <FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' /> |  | ||||||
|           </div> |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | @ -115,7 +207,16 @@ class Results extends PureComponent { | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div className='explore__search-results'> |         <div className='explore__search-results'> | ||||||
|           {isLoading ? <LoadingIndicator /> : filteredResults} |           <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> |         </div> | ||||||
| 
 | 
 | ||||||
|         <Helmet> |         <Helmet> | ||||||
|  |  | ||||||
|  | @ -600,10 +600,9 @@ | ||||||
|   "search_results.all": "All", |   "search_results.all": "All", | ||||||
|   "search_results.hashtags": "Hashtags", |   "search_results.hashtags": "Hashtags", | ||||||
|   "search_results.nothing_found": "Could not find anything for these search terms", |   "search_results.nothing_found": "Could not find anything for these search terms", | ||||||
|  |   "search_results.see_all": "See all", | ||||||
|   "search_results.statuses": "Posts", |   "search_results.statuses": "Posts", | ||||||
|   "search_results.statuses_fts_disabled": "Searching posts by their content is not enabled on this Mastodon server.", |  | ||||||
|   "search_results.title": "Search for {q}", |   "search_results.title": "Search for {q}", | ||||||
|   "search_results.total": "{count, number} {count, plural, one {result} other {results}}", |  | ||||||
|   "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)", |   "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)", | ||||||
|   "server_banner.active_users": "active users", |   "server_banner.active_users": "active users", | ||||||
|   "server_banner.administered_by": "Administered by:", |   "server_banner.administered_by": "Administered by:", | ||||||
|  | @ -675,8 +674,6 @@ | ||||||
|   "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.", |   "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.", | ||||||
|   "subscribed_languages.save": "Save changes", |   "subscribed_languages.save": "Save changes", | ||||||
|   "subscribed_languages.target": "Change subscribed languages for {target}", |   "subscribed_languages.target": "Change subscribed languages for {target}", | ||||||
|   "suggestions.dismiss": "Dismiss suggestion", |  | ||||||
|   "suggestions.header": "You might be interested in…", |  | ||||||
|   "tabs_bar.home": "Home", |   "tabs_bar.home": "Home", | ||||||
|   "tabs_bar.notifications": "Notifications", |   "tabs_bar.notifications": "Notifications", | ||||||
|   "time_remaining.days": "{number, plural, one {# day} other {# days}} left", |   "time_remaining.days": "{number, plural, one {# day} other {# days}} left", | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; | import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|   COMPOSE_MENTION, |   COMPOSE_MENTION, | ||||||
|  | @ -12,6 +12,7 @@ import { | ||||||
|   SEARCH_FETCH_FAIL, |   SEARCH_FETCH_FAIL, | ||||||
|   SEARCH_FETCH_SUCCESS, |   SEARCH_FETCH_SUCCESS, | ||||||
|   SEARCH_SHOW, |   SEARCH_SHOW, | ||||||
|  |   SEARCH_EXPAND_REQUEST, | ||||||
|   SEARCH_EXPAND_SUCCESS, |   SEARCH_EXPAND_SUCCESS, | ||||||
|   SEARCH_RESULT_CLICK, |   SEARCH_RESULT_CLICK, | ||||||
|   SEARCH_RESULT_FORGET, |   SEARCH_RESULT_FORGET, | ||||||
|  | @ -24,6 +25,7 @@ const initialState = ImmutableMap({ | ||||||
|   results: ImmutableMap(), |   results: ImmutableMap(), | ||||||
|   isLoading: false, |   isLoading: false, | ||||||
|   searchTerm: '', |   searchTerm: '', | ||||||
|  |   type: null, | ||||||
|   recent: ImmutableOrderedSet(), |   recent: ImmutableOrderedSet(), | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -37,6 +39,8 @@ export default function search(state = initialState, action) { | ||||||
|       map.set('results', ImmutableMap()); |       map.set('results', ImmutableMap()); | ||||||
|       map.set('submitted', false); |       map.set('submitted', false); | ||||||
|       map.set('hidden', false); |       map.set('hidden', false); | ||||||
|  |       map.set('searchTerm', ''); | ||||||
|  |       map.set('type', null); | ||||||
|     }); |     }); | ||||||
|   case SEARCH_SHOW: |   case SEARCH_SHOW: | ||||||
|     return state.set('hidden', false); |     return state.set('hidden', false); | ||||||
|  | @ -48,23 +52,27 @@ export default function search(state = initialState, action) { | ||||||
|     return state.withMutations(map => { |     return state.withMutations(map => { | ||||||
|       map.set('isLoading', true); |       map.set('isLoading', true); | ||||||
|       map.set('submitted', true); |       map.set('submitted', true); | ||||||
|  |       map.set('type', action.searchType); | ||||||
|     }); |     }); | ||||||
|   case SEARCH_FETCH_FAIL: |   case SEARCH_FETCH_FAIL: | ||||||
|     return state.set('isLoading', false); |     return state.set('isLoading', false); | ||||||
|   case SEARCH_FETCH_SUCCESS: |   case SEARCH_FETCH_SUCCESS: | ||||||
|     return state.withMutations(map => { |     return state.withMutations(map => { | ||||||
|       map.set('results', ImmutableMap({ |       map.set('results', ImmutableMap({ | ||||||
|         accounts: ImmutableList(action.results.accounts.map(item => item.id)), |         accounts: ImmutableOrderedSet(action.results.accounts.map(item => item.id)), | ||||||
|         statuses: ImmutableList(action.results.statuses.map(item => item.id)), |         statuses: ImmutableOrderedSet(action.results.statuses.map(item => item.id)), | ||||||
|         hashtags: fromJS(action.results.hashtags), |         hashtags: ImmutableOrderedSet(fromJS(action.results.hashtags)), | ||||||
|       })); |       })); | ||||||
| 
 | 
 | ||||||
|       map.set('searchTerm', action.searchTerm); |       map.set('searchTerm', action.searchTerm); | ||||||
|  |       map.set('type', action.searchType); | ||||||
|       map.set('isLoading', false); |       map.set('isLoading', false); | ||||||
|     }); |     }); | ||||||
|  |   case SEARCH_EXPAND_REQUEST: | ||||||
|  |     return state.set('type', action.searchType); | ||||||
|   case SEARCH_EXPAND_SUCCESS: |   case SEARCH_EXPAND_SUCCESS: | ||||||
|     const results = action.searchType === 'hashtags' ? fromJS(action.results.hashtags) : action.results[action.searchType].map(item => item.id); |     const results = action.searchType === 'hashtags' ? ImmutableOrderedSet(fromJS(action.results.hashtags)) : action.results[action.searchType].map(item => item.id); | ||||||
|     return state.updateIn(['results', action.searchType], list => list.concat(results)); |     return state.updateIn(['results', action.searchType], list => list.union(results)); | ||||||
|   case SEARCH_RESULT_CLICK: |   case SEARCH_RESULT_CLICK: | ||||||
|     return state.update('recent', set => set.add(fromJS(action.result))); |     return state.update('recent', set => set.add(fromJS(action.result))); | ||||||
|   case SEARCH_RESULT_FORGET: |   case SEARCH_RESULT_FORGET: | ||||||
|  |  | ||||||
|  | @ -5172,22 +5172,39 @@ a.status-card { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .search-results__section { | .search-results__section { | ||||||
|   margin-bottom: 5px; |   border-bottom: 1px solid lighten($ui-base-color, 8%); | ||||||
| 
 | 
 | ||||||
|   h5 { |   &:last-child { | ||||||
|  |     border-bottom: 0; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &__header { | ||||||
|     background: darken($ui-base-color, 4%); |     background: darken($ui-base-color, 4%); | ||||||
|     border-bottom: 1px solid lighten($ui-base-color, 8%); |     border-bottom: 1px solid lighten($ui-base-color, 8%); | ||||||
|     cursor: default; |  | ||||||
|     display: flex; |  | ||||||
|     padding: 15px; |     padding: 15px; | ||||||
|     font-weight: 500; |     font-weight: 500; | ||||||
|     font-size: 16px; |     font-size: 14px; | ||||||
|     color: $dark-text-color; |     color: $darker-text-color; | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: space-between; | ||||||
| 
 | 
 | ||||||
|     .fa { |     h3 .fa { | ||||||
|       display: inline-block; |  | ||||||
|       margin-inline-end: 5px; |       margin-inline-end: 5px; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     button { | ||||||
|  |       color: $highlight-text-color; | ||||||
|  |       padding: 0; | ||||||
|  |       border: 0; | ||||||
|  |       background: 0; | ||||||
|  |       font: inherit; | ||||||
|  | 
 | ||||||
|  |       &:hover, | ||||||
|  |       &:active, | ||||||
|  |       &:focus { | ||||||
|  |         text-decoration: underline; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .account:last-child, |   .account:last-child, | ||||||
|  | @ -6815,14 +6832,14 @@ a.status-card { | ||||||
| 
 | 
 | ||||||
| .notification__filter-bar, | .notification__filter-bar, | ||||||
| .account__section-headline { | .account__section-headline { | ||||||
|   background: darken($ui-base-color, 4%); |   background: $ui-base-color; | ||||||
|   border-bottom: 1px solid lighten($ui-base-color, 8%); |   border-bottom: 1px solid lighten($ui-base-color, 8%); | ||||||
|   cursor: default; |   cursor: default; | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-shrink: 0; |   flex-shrink: 0; | ||||||
| 
 | 
 | ||||||
|   button { |   button { | ||||||
|     background: darken($ui-base-color, 4%); |     background: transparent; | ||||||
|     border: 0; |     border: 0; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|   } |   } | ||||||
|  | @ -6842,26 +6859,18 @@ a.status-card { | ||||||
|     white-space: nowrap; |     white-space: nowrap; | ||||||
| 
 | 
 | ||||||
|     &.active { |     &.active { | ||||||
|       color: $secondary-text-color; |       color: $primary-text-color; | ||||||
| 
 | 
 | ||||||
|       &::before, |       &::before { | ||||||
|       &::after { |  | ||||||
|         display: block; |         display: block; | ||||||
|         content: ''; |         content: ''; | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         bottom: 0; |  | ||||||
|         left: 50%; |  | ||||||
|         width: 0; |  | ||||||
|         height: 0; |  | ||||||
|         transform: translateX(-50%); |  | ||||||
|         border-style: solid; |  | ||||||
|         border-width: 0 10px 10px; |  | ||||||
|         border-color: transparent transparent lighten($ui-base-color, 8%); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       &::after { |  | ||||||
|         bottom: -1px; |         bottom: -1px; | ||||||
|         border-color: transparent transparent $ui-base-color; |         left: 0; | ||||||
|  |         width: 100%; | ||||||
|  |         height: 3px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         background: $highlight-text-color; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue