From d0f3fe6771fc23a95296cad611ba61605650006e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 30 Jul 2021 17:51:43 +0200 Subject: [PATCH 1/5] Tabbed, paginated search results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/search.js | 56 ++++++++++++++++-- .../compose/components/search_results.js | 57 ++++++++++++------- .../containers/search_results_container.js | 14 +++-- .../features/search/components/filter_bar.js | 53 +++++++++++++++++ app/soapbox/reducers/search.js | 9 +++ app/soapbox/reducers/settings.js | 2 + app/styles/components/search.scss | 6 +- app/styles/ui.scss | 1 + 8 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 app/soapbox/features/search/components/filter_bar.js diff --git a/app/soapbox/actions/search.js b/app/soapbox/actions/search.js index ee7bc1657..c974819fc 100644 --- a/app/soapbox/actions/search.js +++ b/app/soapbox/actions/search.js @@ -10,6 +10,12 @@ export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; +export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST'; +export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS'; +export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL'; + +export const SEARCH_FILTER_SET = 'SEARCH_FILTER_SET'; + export function changeSearch(value) { return { type: SEARCH_CHANGE, @@ -76,8 +82,50 @@ export function fetchSearchFail(error) { }; }; -export function showSearch() { - return { - type: SEARCH_SHOW, - }; +export const expandSearch = type => (dispatch, getState) => { + const value = getState().getIn(['search', 'value']); + const offset = getState().getIn(['search', 'results', type]).size; + + dispatch(expandSearchRequest()); + + api(getState).get('/api/v2/search', { + params: { + q: value, + type, + offset, + }, + }).then(({ data }) => { + if (data.accounts) { + dispatch(importFetchedAccounts(data.accounts)); + } + + if (data.statuses) { + dispatch(importFetchedStatuses(data.statuses)); + } + + dispatch(expandSearchSuccess(data, value, type)); + dispatch(fetchRelationships(data.accounts.map(item => item.id))); + }).catch(error => { + dispatch(expandSearchFail(error)); + }); }; + +export const expandSearchRequest = () => ({ + type: SEARCH_EXPAND_REQUEST, +}); + +export const expandSearchSuccess = (results, searchTerm, searchType) => ({ + type: SEARCH_EXPAND_SUCCESS, + results, + searchTerm, + searchType, +}); + +export const expandSearchFail = error => ({ + type: SEARCH_EXPAND_FAIL, + error, +}); + +export const showSearch = () => ({ + type: SEARCH_SHOW, +}); diff --git a/app/soapbox/features/compose/components/search_results.js b/app/soapbox/features/compose/components/search_results.js index c7838f96a..c6d990734 100644 --- a/app/soapbox/features/compose/components/search_results.js +++ b/app/soapbox/features/compose/components/search_results.js @@ -8,6 +8,9 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import Hashtag from '../../../components/hashtag'; import Icon from 'soapbox/components/icon'; import LoadingIndicator from 'soapbox/components/loading_indicator'; +import FilterBar from '../../search/components/filter_bar'; +import LoadMore from '../../../components/load_more'; +import classNames from 'classnames'; export default @injectIntl class SearchResults extends ImmutablePureComponent { @@ -15,11 +18,24 @@ class SearchResults extends ImmutablePureComponent { static propTypes = { results: ImmutablePropTypes.map.isRequired, submitted: PropTypes.bool, + expandSearch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; + state = { + selectedFilter: 'accounts', + }; + + handleLoadMore = () => this.props.expandSearch(this.state.selectedFilter); + + handleSelectFilter = newActiveFilter => { + console.log(newActiveFilter); + this.setState({ selectedFilter: newActiveFilter }); + }; + render() { const { results, submitted } = this.props; + const { selectedFilter } = this.state; if (submitted && results.isEmpty()) { return ( @@ -29,37 +45,38 @@ class SearchResults extends ImmutablePureComponent { ); } - let accounts, statuses, hashtags; + let searchResults; let count = 0; + let hasMore = false; - if (results.get('accounts') && results.get('accounts').size > 0) { - count += results.get('accounts').size; - accounts = ( -
-
+ if (selectedFilter === 'accounts' && results.get('accounts') && results.get('accounts').size > 0) { + count = results.get('accounts').size; + hasMore = results.get('accountsHasMore'); + searchResults = ( +
{results.get('accounts').map(accountId => )}
); } - if (results.get('statuses') && results.get('statuses').size > 0) { - count += results.get('statuses').size; - statuses = ( -
-
+ if (selectedFilter === 'statuses' && results.get('statuses') && results.get('statuses').size > 0) { + count = results.get('statuses').size; + hasMore = results.get('statusesHasMore'); + searchResults = ( +
{results.get('statuses').map(statusId => )}
); } - if (results.get('hashtags') && results.get('hashtags').size > 0) { - count += results.get('hashtags').size; - hashtags = ( -
-
+ if (selectedFilter === 'hashtags' && results.get('hashtags') && results.get('hashtags').size > 0) { + count = results.get('hashtags').size; + hasMore = results.get('hashtagsHasMore'); + searchResults = ( +
{results.get('hashtags').map(hashtag => )}
); @@ -72,9 +89,11 @@ class SearchResults extends ImmutablePureComponent {
- {accounts} - {statuses} - {hashtags} + + + {searchResults} + + {hasMore && }
); } diff --git a/app/soapbox/features/compose/containers/search_results_container.js b/app/soapbox/features/compose/containers/search_results_container.js index 046e374ac..734612dce 100644 --- a/app/soapbox/features/compose/containers/search_results_container.js +++ b/app/soapbox/features/compose/containers/search_results_container.js @@ -1,15 +1,19 @@ import { connect } from 'react-redux'; import SearchResults from '../components/search_results'; import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions'; +import { expandSearch } from '../../../actions/search'; -const mapStateToProps = state => ({ - results: state.getIn(['search', 'results']), - suggestions: state.getIn(['suggestions', 'items']), - submitted: state.getIn(['search', 'submitted']), -}); +const mapStateToProps = state => { + return { + results: state.getIn(['search', 'results']), + suggestions: state.getIn(['suggestions', 'items']), + submitted: state.getIn(['search', 'submitted']), + }; +}; const mapDispatchToProps = dispatch => ({ fetchSuggestions: () => dispatch(fetchSuggestions()), + expandSearch: type => dispatch(expandSearch(type)), dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))), }); diff --git a/app/soapbox/features/search/components/filter_bar.js b/app/soapbox/features/search/components/filter_bar.js new file mode 100644 index 000000000..917ad99c7 --- /dev/null +++ b/app/soapbox/features/search/components/filter_bar.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, injectIntl } from 'react-intl'; + +export default @injectIntl +class FilterBar extends React.PureComponent { + + static propTypes = { + selectFilter: PropTypes.func.isRequired, + selectedFilter: PropTypes.string.isRequired, + }; + + onClick(searchType) { + return () => this.props.selectFilter(searchType); + } + + render() { + const { selectedFilter } = this.props; + + return ( +
+ + + +
+ ); + } + +} diff --git a/app/soapbox/reducers/search.js b/app/soapbox/reducers/search.js index 4a0a729a1..7a44956c2 100644 --- a/app/soapbox/reducers/search.js +++ b/app/soapbox/reducers/search.js @@ -4,6 +4,7 @@ import { SEARCH_FETCH_REQUEST, SEARCH_FETCH_SUCCESS, SEARCH_SHOW, + SEARCH_EXPAND_SUCCESS, } from '../actions/search'; import { COMPOSE_MENTION, @@ -49,7 +50,15 @@ export default function search(state = initialState, action) { accounts: ImmutableList(action.results.accounts.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)), hashtags: fromJS(action.results.hashtags), + accountsHasMore: action.results.accounts.length > 0, + statusesHasMore: action.results.statuses.length > 0, + hashtagsHasMore: action.results.hashtags.length > 0, })).set('submitted', true); + case SEARCH_EXPAND_SUCCESS: + return state.withMutations((state) => { + state.setIn(['results', `${action.searchType}HasMore`], action.results[action.searchType].length > 0); + state.updateIn(['results', action.searchType], list => list.concat(action.results[action.searchType].map(item => item.id))); + }); default: return state; } diff --git a/app/soapbox/reducers/settings.js b/app/soapbox/reducers/settings.js index a409f5905..343a3f9d9 100644 --- a/app/soapbox/reducers/settings.js +++ b/app/soapbox/reducers/settings.js @@ -1,5 +1,6 @@ import { SETTING_CHANGE, SETTING_SAVE, FE_NAME } from '../actions/settings'; import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications'; +import { SEARCH_FILTER_SET } from '../actions/search'; import { EMOJI_USE } from '../actions/emojis'; import { ME_FETCH_SUCCESS } from 'soapbox/actions/me'; import { Map as ImmutableMap, fromJS } from 'immutable'; @@ -25,6 +26,7 @@ export default function settings(state = initialState, action) { case ME_FETCH_SUCCESS: return importSettings(state, action.me); case NOTIFICATIONS_FILTER_SET: + case SEARCH_FILTER_SET: case SETTING_CHANGE: return state .setIn(action.path, action.value) diff --git a/app/styles/components/search.scss b/app/styles/components/search.scss index 6933c6006..ffc46b2b0 100644 --- a/app/styles/components/search.scss +++ b/app/styles/components/search.scss @@ -68,8 +68,6 @@ } .search-results__section { - margin-bottom: 5px; - h5 { background: var(--accent-color--faint); border-bottom: 1px solid var(--brand-color--faint); @@ -86,8 +84,8 @@ } } - .account:last-child, - & > div:last-child .status { + &:not(.has-more) .account:last-child, + &:not(.has-more) > div:last-child .status { border-bottom: 0; } } diff --git a/app/styles/ui.scss b/app/styles/ui.scss index 5a6b3e554..86cf3361d 100644 --- a/app/styles/ui.scss +++ b/app/styles/ui.scss @@ -608,6 +608,7 @@ } .notification__filter-bar, +.search__filter-bar, .account__section-headline { border-bottom: 1px solid var(--brand-color--faint); cursor: default; From e7d122dc95868dac32ca0f13031f4c3a87a3472f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 30 Jul 2021 21:49:28 +0200 Subject: [PATCH 2/5] No more items if there are less than 20 results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/reducers/search.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/reducers/search.js b/app/soapbox/reducers/search.js index 7a44956c2..c9ad6bfa8 100644 --- a/app/soapbox/reducers/search.js +++ b/app/soapbox/reducers/search.js @@ -50,13 +50,13 @@ export default function search(state = initialState, action) { accounts: ImmutableList(action.results.accounts.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)), hashtags: fromJS(action.results.hashtags), - accountsHasMore: action.results.accounts.length > 0, - statusesHasMore: action.results.statuses.length > 0, - hashtagsHasMore: action.results.hashtags.length > 0, + accountsHasMore: action.results.accounts.length >= 20, + statusesHasMore: action.results.statuses.length >= 20, + hashtagsHasMore: action.results.hashtags.length >= 20, })).set('submitted', true); case SEARCH_EXPAND_SUCCESS: return state.withMutations((state) => { - state.setIn(['results', `${action.searchType}HasMore`], action.results[action.searchType].length > 0); + state.setIn(['results', `${action.searchType}HasMore`], action.results[action.searchType].length >= 20); state.updateIn(['results', action.searchType], list => list.concat(action.results[action.searchType].map(item => item.id))); }); default: From 29d68dac06f9b3bd8590a27d00142235b1a58561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 30 Jul 2021 21:54:56 +0200 Subject: [PATCH 3/5] Show 'Over X results' if more results are available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/compose/components/search_results.js | 6 +++++- app/soapbox/locales/pl.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/compose/components/search_results.js b/app/soapbox/features/compose/components/search_results.js index c6d990734..45de8036d 100644 --- a/app/soapbox/features/compose/components/search_results.js +++ b/app/soapbox/features/compose/components/search_results.js @@ -86,7 +86,11 @@ class SearchResults extends ImmutablePureComponent {
- + { + hasMore + ? + : + }
diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index b968895d7..12611d8b4 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -616,6 +616,7 @@ "search_results.statuses": "Wpisy", "search_results.top": "Góra", "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", + "search_results.total.has_more": "{count, number} Ponad {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", "security.codes.fail": "Nie udało się uzyskać zapasowych kodów", "security.confirm.fail": "Nieprawidłowy kod lub hasło. Spróbuj ponownie.", "security.delete_account.fail": "Nie udało się usunąć konta.", From 055b001f74d8ecf93c2adcc34be118fd7ffb449b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 30 Jul 2021 23:05:54 +0200 Subject: [PATCH 4/5] Works fine, I think MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../compose/components/search_results.js | 26 +++---------------- app/soapbox/features/search/index.js | 16 ++++++++++-- app/styles/components/search.scss | 15 +++++++++++ 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/app/soapbox/features/compose/components/search_results.js b/app/soapbox/features/compose/components/search_results.js index 45de8036d..5e8b31b8b 100644 --- a/app/soapbox/features/compose/components/search_results.js +++ b/app/soapbox/features/compose/components/search_results.js @@ -1,25 +1,21 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage, injectIntl } from 'react-intl'; import AccountContainer from '../../../containers/account_container'; import StatusContainer from '../../../containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Hashtag from '../../../components/hashtag'; -import Icon from 'soapbox/components/icon'; import LoadingIndicator from 'soapbox/components/loading_indicator'; import FilterBar from '../../search/components/filter_bar'; import LoadMore from '../../../components/load_more'; import classNames from 'classnames'; -export default @injectIntl -class SearchResults extends ImmutablePureComponent { +export default class SearchResults extends ImmutablePureComponent { static propTypes = { results: ImmutablePropTypes.map.isRequired, submitted: PropTypes.bool, expandSearch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, }; state = { @@ -29,7 +25,6 @@ class SearchResults extends ImmutablePureComponent { handleLoadMore = () => this.props.expandSearch(this.state.selectedFilter); handleSelectFilter = newActiveFilter => { - console.log(newActiveFilter); this.setState({ selectedFilter: newActiveFilter }); }; @@ -46,11 +41,9 @@ class SearchResults extends ImmutablePureComponent { } let searchResults; - let count = 0; let hasMore = false; if (selectedFilter === 'accounts' && results.get('accounts') && results.get('accounts').size > 0) { - count = results.get('accounts').size; hasMore = results.get('accountsHasMore'); searchResults = ( @@ -61,7 +54,6 @@ class SearchResults extends ImmutablePureComponent { } if (selectedFilter === 'statuses' && results.get('statuses') && results.get('statuses').size > 0) { - count = results.get('statuses').size; hasMore = results.get('statusesHasMore'); searchResults = ( @@ -72,7 +64,6 @@ class SearchResults extends ImmutablePureComponent { } if (selectedFilter === 'hashtags' && results.get('hashtags') && results.get('hashtags').size > 0) { - count = results.get('hashtags').size; hasMore = results.get('hashtagsHasMore'); searchResults = ( @@ -83,22 +74,13 @@ class SearchResults extends ImmutablePureComponent { } return ( -
-
- - { - hasMore - ? - : - } -
- - + <> + {searchResults} {hasMore && } -
+ ); } diff --git a/app/soapbox/features/search/index.js b/app/soapbox/features/search/index.js index 63ea292a3..22a898d78 100644 --- a/app/soapbox/features/search/index.js +++ b/app/soapbox/features/search/index.js @@ -1,11 +1,19 @@ import React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import PropTypes from 'prop-types'; +import ColumnHeader from 'soapbox/components/column_header'; import SearchContainer from 'soapbox/features/compose/containers/search_container'; import SearchResultsContainer from 'soapbox/features/compose/containers/search_results_container'; -const Search = () => ( +const messages = defineMessages({ + heading: { id: 'column.search', defaultMessage: 'Search' }, +}); + +const Search = ({ intl }) => (
+
@@ -14,4 +22,8 @@ const Search = () => (
); -export default Search; +Search.propTypes = { + intl: PropTypes.object.isRequired, +}; + +export default injectIntl(Search); diff --git a/app/styles/components/search.scss b/app/styles/components/search.scss index ffc46b2b0..a3416438b 100644 --- a/app/styles/components/search.scss +++ b/app/styles/components/search.scss @@ -1,9 +1,19 @@ +.search-page { + min-height: 97px; +} + @media screen and (min-width: 600px + (285px * 1) + (10px * 1)) { .search-page .search { display: none; } } +@media screen and (max-width: 600px + (285px * 1) + (10px * 1) - 1px) { + .search-page .column-header__wrapper { + display: none; + } +} + .search { position: relative; } @@ -158,3 +168,8 @@ .search-popout { @include search-popout; } + + +.search__filter-bar:last-child { + border-bottom: none; +} \ No newline at end of file From 1fd9949566b009d44946d972f21347025d64f39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 30 Jul 2021 23:22:55 +0200 Subject: [PATCH 5/5] Lint styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/styles/components/search.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/styles/components/search.scss b/app/styles/components/search.scss index a3416438b..84d611fd8 100644 --- a/app/styles/components/search.scss +++ b/app/styles/components/search.scss @@ -169,7 +169,6 @@ @include search-popout; } - .search__filter-bar:last-child { border-bottom: none; -} \ No newline at end of file +}