forked from mastodon/mastodon
-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add infinite scrolling for search results in web UI (mastodon#26784)
- Loading branch information
Showing
7 changed files
with
253 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 31 additions & 86 deletions
117
app/javascript/mastodon/features/compose/components/search_results.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,144 +1,89 @@ | ||
import PropTypes from 'prop-types'; | ||
|
||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; | ||
import { FormattedMessage } from 'react-intl'; | ||
|
||
import ImmutablePropTypes from 'react-immutable-proptypes'; | ||
import ImmutablePureComponent from 'react-immutable-pure-component'; | ||
|
||
import { Icon } from 'mastodon/components/icon'; | ||
import { LoadMore } from 'mastodon/components/load_more'; | ||
import { SearchSection } from 'mastodon/features/explore/components/search_section'; | ||
|
||
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag'; | ||
import AccountContainer from '../../../containers/account_container'; | ||
import StatusContainer from '../../../containers/status_container'; | ||
import { searchEnabled } from '../../../initial_state'; | ||
|
||
const messages = defineMessages({ | ||
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, | ||
}); | ||
const INITIAL_PAGE_LIMIT = 10; | ||
|
||
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 { | ||
|
||
static propTypes = { | ||
results: ImmutablePropTypes.map.isRequired, | ||
suggestions: ImmutablePropTypes.list.isRequired, | ||
fetchSuggestions: PropTypes.func.isRequired, | ||
expandSearch: PropTypes.func.isRequired, | ||
dismissSuggestion: PropTypes.func.isRequired, | ||
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'); | ||
|
||
handleLoadMoreStatuses = () => this.props.expandSearch('statuses'); | ||
|
||
handleLoadMoreHashtags = () => this.props.expandSearch('hashtags'); | ||
|
||
render () { | ||
const { intl, results, suggestions, dismissSuggestion, searchTerm } = 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> | ||
); | ||
} | ||
const { results } = this.props; | ||
|
||
let accounts, statuses, hashtags; | ||
let count = 0; | ||
|
||
if (results.get('accounts') && results.get('accounts').size > 0) { | ||
count += results.get('accounts').size; | ||
accounts = ( | ||
<div className='search-results__section'> | ||
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></h5> | ||
|
||
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} | ||
<SearchSection title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}> | ||
{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} />} | ||
</SearchSection> | ||
); | ||
} | ||
|
||
{results.get('accounts').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreAccounts} />} | ||
</div> | ||
if (results.get('hashtags') && results.get('hashtags').size > 0) { | ||
hashtags = ( | ||
<SearchSection title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}> | ||
{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} />} | ||
</SearchSection> | ||
); | ||
} | ||
|
||
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> | ||
<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> | ||
); | ||
} | ||
|
||
if (results.get('hashtags') && results.get('hashtags').size > 0) { | ||
count += results.get('hashtags').size; | ||
hashtags = ( | ||
<div className='search-results__section'> | ||
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> | ||
|
||
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} | ||
|
||
{results.get('hashtags').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreHashtags} />} | ||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<div className='search-results'> | ||
<div className='search-results__header'> | ||
<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> | ||
|
||
{accounts} | ||
{statuses} | ||
{hashtags} | ||
{statuses} | ||
</div> | ||
); | ||
} | ||
|
||
} | ||
|
||
export default injectIntl(SearchResults); | ||
export default SearchResults; |
20 changes: 20 additions & 0 deletions
20
app/javascript/mastodon/features/explore/components/search_section.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}; |
Oops, something went wrong.