Merge branch 'search-from-account' into 'develop'
Allow to search for posts from given account See merge request soapbox-pub/soapbox-fe!1710
This commit is contained in:
commit
a2f9c7d97b
|
@ -22,6 +22,8 @@ const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
|
||||||
const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
|
const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
|
||||||
const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
|
const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
|
||||||
|
|
||||||
|
const SEARCH_ACCOUNT_SET = 'SEARCH_ACCOUNT_SET';
|
||||||
|
|
||||||
const changeSearch = (value: string) =>
|
const changeSearch = (value: string) =>
|
||||||
(dispatch: AppDispatch) => {
|
(dispatch: AppDispatch) => {
|
||||||
// If backspaced all the way, clear the search
|
// If backspaced all the way, clear the search
|
||||||
|
@ -43,6 +45,7 @@ const submitSearch = (filter?: SearchFilter) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const value = getState().search.value;
|
const value = getState().search.value;
|
||||||
const type = filter || getState().search.filter || 'accounts';
|
const type = filter || getState().search.filter || 'accounts';
|
||||||
|
const accountId = getState().search.accountId;
|
||||||
|
|
||||||
// An empty search doesn't return any results
|
// An empty search doesn't return any results
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
|
@ -51,13 +54,17 @@ const submitSearch = (filter?: SearchFilter) =>
|
||||||
|
|
||||||
dispatch(fetchSearchRequest(value));
|
dispatch(fetchSearchRequest(value));
|
||||||
|
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
q: value,
|
||||||
|
resolve: true,
|
||||||
|
limit: 20,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (accountId) params.account_id = accountId;
|
||||||
|
|
||||||
api(getState).get('/api/v2/search', {
|
api(getState).get('/api/v2/search', {
|
||||||
params: {
|
params,
|
||||||
q: value,
|
|
||||||
resolve: true,
|
|
||||||
limit: 20,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
if (response.data.accounts) {
|
if (response.data.accounts) {
|
||||||
dispatch(importFetchedAccounts(response.data.accounts));
|
dispatch(importFetchedAccounts(response.data.accounts));
|
||||||
|
@ -151,6 +158,11 @@ const showSearch = () => ({
|
||||||
type: SEARCH_SHOW,
|
type: SEARCH_SHOW,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setSearchAccount = (accountId: string) => ({
|
||||||
|
type: SEARCH_ACCOUNT_SET,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SEARCH_CHANGE,
|
SEARCH_CHANGE,
|
||||||
SEARCH_CLEAR,
|
SEARCH_CLEAR,
|
||||||
|
@ -162,6 +174,7 @@ export {
|
||||||
SEARCH_EXPAND_REQUEST,
|
SEARCH_EXPAND_REQUEST,
|
||||||
SEARCH_EXPAND_SUCCESS,
|
SEARCH_EXPAND_SUCCESS,
|
||||||
SEARCH_EXPAND_FAIL,
|
SEARCH_EXPAND_FAIL,
|
||||||
|
SEARCH_ACCOUNT_SET,
|
||||||
changeSearch,
|
changeSearch,
|
||||||
clearSearch,
|
clearSearch,
|
||||||
submitSearch,
|
submitSearch,
|
||||||
|
@ -174,4 +187,5 @@ export {
|
||||||
expandSearchSuccess,
|
expandSearchSuccess,
|
||||||
expandSearchFail,
|
expandSearchFail,
|
||||||
showSearch,
|
showSearch,
|
||||||
|
setSearchAccount,
|
||||||
};
|
};
|
||||||
|
|
|
@ -64,6 +64,7 @@ const messages = defineMessages({
|
||||||
demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
|
demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
|
||||||
suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' },
|
suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' },
|
||||||
unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' },
|
unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' },
|
||||||
|
search: { id: 'account.search', defaultMessage: 'Search from @{name}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
|
@ -274,6 +275,14 @@ class Header extends ImmutablePureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (features.searchFromAccount) {
|
||||||
|
menu.push({
|
||||||
|
text: intl.formatMessage(messages.search, { name: account.get('username') }),
|
||||||
|
action: this.props.onSearch,
|
||||||
|
icon: require('@tabler/icons/search.svg'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (features.removeFromFollowers && account.relationship?.followed_by) {
|
if (features.removeFromFollowers && account.relationship?.followed_by) {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: intl.formatMessage(messages.removeFromFollowers),
|
text: intl.formatMessage(messages.removeFromFollowers),
|
||||||
|
|
|
@ -26,6 +26,7 @@ class Header extends ImmutablePureComponent {
|
||||||
onEndorseToggle: PropTypes.func.isRequired,
|
onEndorseToggle: PropTypes.func.isRequired,
|
||||||
onAddToList: PropTypes.func.isRequired,
|
onAddToList: PropTypes.func.isRequired,
|
||||||
onRemoveFromFollowers: PropTypes.func.isRequired,
|
onRemoveFromFollowers: PropTypes.func.isRequired,
|
||||||
|
onSearch: PropTypes.func.isRequired,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
@ -146,6 +147,10 @@ class Header extends ImmutablePureComponent {
|
||||||
this.props.onRemoveFromFollowers(this.props.account);
|
this.props.onRemoveFromFollowers(this.props.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSearch = () => {
|
||||||
|
this.props.onSearch(this.props.account, this.props.history);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { account } = this.props;
|
const { account } = this.props;
|
||||||
const moved = (account) ? account.get('moved') : false;
|
const moved = (account) ? account.get('moved') : false;
|
||||||
|
@ -183,6 +188,7 @@ class Header extends ImmutablePureComponent {
|
||||||
onUnsuggestUser={this.handleUnsuggestUser}
|
onUnsuggestUser={this.handleUnsuggestUser}
|
||||||
onShowNote={this.handleShowNote}
|
onShowNote={this.handleShowNote}
|
||||||
onRemoveFromFollowers={this.handleRemoveFromFollowers}
|
onRemoveFromFollowers={this.handleRemoveFromFollowers}
|
||||||
|
onSearch={this.handleSearch}
|
||||||
username={this.props.username}
|
username={this.props.username}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { openModal } from 'soapbox/actions/modals';
|
||||||
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
||||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||||
import { initReport } from 'soapbox/actions/reports';
|
import { initReport } from 'soapbox/actions/reports';
|
||||||
|
import { setSearchAccount } from 'soapbox/actions/search';
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
import { makeGetAccount } from 'soapbox/selectors';
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
@ -291,6 +292,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onSearch(account, router) {
|
||||||
|
dispatch((dispatch) => {
|
||||||
|
dispatch(setSearchAccount(account.id));
|
||||||
|
router.push('/search');
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
||||||
|
|
|
@ -2,11 +2,12 @@ import classNames from 'classnames';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { expandSearch, setFilter } from 'soapbox/actions/search';
|
import { clearSearch, expandSearch, setFilter } from 'soapbox/actions/search';
|
||||||
import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses';
|
import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses';
|
||||||
import Hashtag from 'soapbox/components/hashtag';
|
import Hashtag from 'soapbox/components/hashtag';
|
||||||
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||||
import { Tabs } from 'soapbox/components/ui';
|
import { HStack, Tabs, Text } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account_container';
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
import StatusContainer from 'soapbox/containers/status_container';
|
import StatusContainer from 'soapbox/containers/status_container';
|
||||||
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
|
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
|
||||||
|
@ -37,9 +38,13 @@ const SearchResults = () => {
|
||||||
const trends = useAppSelector((state) => state.trends.items);
|
const trends = useAppSelector((state) => state.trends.items);
|
||||||
const submitted = useAppSelector((state) => state.search.submitted);
|
const submitted = useAppSelector((state) => state.search.submitted);
|
||||||
const selectedFilter = useAppSelector((state) => state.search.filter);
|
const selectedFilter = useAppSelector((state) => state.search.filter);
|
||||||
|
const filterByAccount = useAppSelector((state) => state.search.accountId);
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(filterByAccount)?.acct);
|
||||||
|
|
||||||
const handleLoadMore = () => dispatch(expandSearch(selectedFilter));
|
const handleLoadMore = () => dispatch(expandSearch(selectedFilter));
|
||||||
|
|
||||||
|
const handleClearSearch = () => dispatch(clearSearch());
|
||||||
|
|
||||||
const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter));
|
const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter));
|
||||||
|
|
||||||
const renderFilterBar = () => {
|
const renderFilterBar = () => {
|
||||||
|
@ -189,7 +194,18 @@ const SearchResults = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderFilterBar()}
|
{filterByAccount ? (
|
||||||
|
<HStack className='mb-4 pb-4 px-2 border-solid border-b border-gray-200 dark:border-gray-800' space={2}>
|
||||||
|
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/x.svg')} onClick={handleClearSearch} />
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage
|
||||||
|
id='search_results.filter_message'
|
||||||
|
defaultMessage='You are searching for posts from @{acct}.'
|
||||||
|
values={{ acct: account }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
) : renderFilterBar()}
|
||||||
|
|
||||||
{noResultsMessage || (
|
{noResultsMessage || (
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
|
|
|
@ -27,6 +27,7 @@ describe('search reducer', () => {
|
||||||
hashtagsLoaded: false,
|
hashtagsLoaded: false,
|
||||||
},
|
},
|
||||||
filter: 'accounts',
|
filter: 'accounts',
|
||||||
|
accountId: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -68,6 +69,7 @@ describe('search reducer', () => {
|
||||||
hashtagsLoaded: false,
|
hashtagsLoaded: false,
|
||||||
},
|
},
|
||||||
filter: 'accounts',
|
filter: 'accounts',
|
||||||
|
accountId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(reducer(state, action).toJS()).toEqual(expected);
|
expect(reducer(state, action).toJS()).toEqual(expected);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
SEARCH_FILTER_SET,
|
SEARCH_FILTER_SET,
|
||||||
SEARCH_EXPAND_REQUEST,
|
SEARCH_EXPAND_REQUEST,
|
||||||
SEARCH_EXPAND_SUCCESS,
|
SEARCH_EXPAND_SUCCESS,
|
||||||
|
SEARCH_ACCOUNT_SET,
|
||||||
} from '../actions/search';
|
} from '../actions/search';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
|
@ -41,6 +42,7 @@ const ReducerRecord = ImmutableRecord({
|
||||||
hidden: false,
|
hidden: false,
|
||||||
results: ResultsRecord(),
|
results: ResultsRecord(),
|
||||||
filter: 'accounts' as SearchFilter,
|
filter: 'accounts' as SearchFilter,
|
||||||
|
accountId: null as string | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
type State = ReturnType<typeof ReducerRecord>;
|
type State = ReturnType<typeof ReducerRecord>;
|
||||||
|
@ -120,6 +122,8 @@ export default function search(state = ReducerRecord(), action: AnyAction) {
|
||||||
return state.setIn(['results', `${action.searchType}Loaded`], false);
|
return state.setIn(['results', `${action.searchType}Loaded`], false);
|
||||||
case SEARCH_EXPAND_SUCCESS:
|
case SEARCH_EXPAND_SUCCESS:
|
||||||
return paginateResults(state, action.searchType, action.results, action.searchTerm);
|
return paginateResults(state, action.searchType, action.results, action.searchTerm);
|
||||||
|
case SEARCH_ACCOUNT_SET:
|
||||||
|
return ReducerRecord({ accountId: action.accountId, filter: 'statuses' });
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -536,6 +536,16 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
scopes: v.software === PLEROMA ? 'read write follow push admin' : 'read write follow push',
|
scopes: v.software === PLEROMA ? 'read write follow push admin' : 'read write follow push',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ability to search statuses from the given account.
|
||||||
|
* @see {@link https://docs.joinmastodon.org/methods/search/}
|
||||||
|
* @see POST /api/v2/search
|
||||||
|
*/
|
||||||
|
searchFromAccount: any([
|
||||||
|
v.software === MASTODON && gte(v.version, '2.8.0'),
|
||||||
|
v.software === PLEROMA && gte(v.version, '1.0.0'),
|
||||||
|
]),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ability to manage account security settings.
|
* Ability to manage account security settings.
|
||||||
* @see POST /api/pleroma/change_password
|
* @see POST /api/pleroma/change_password
|
||||||
|
|
Loading…
Reference in New Issue