From 41b01382ac4a3267f2abc037bed4f9104b0e5d17 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 14 May 2022 12:35:12 -0500 Subject: [PATCH 1/6] SearchResults: convert to TSX --- .../compose/components/search_results.js | 174 ---------------- .../compose/components/search_results.tsx | 187 ++++++++++++++++++ 2 files changed, 187 insertions(+), 174 deletions(-) delete mode 100644 app/soapbox/features/compose/components/search_results.js create mode 100644 app/soapbox/features/compose/components/search_results.tsx diff --git a/app/soapbox/features/compose/components/search_results.js b/app/soapbox/features/compose/components/search_results.js deleted file mode 100644 index 05cbe2663..000000000 --- a/app/soapbox/features/compose/components/search_results.js +++ /dev/null @@ -1,174 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage } from 'react-intl'; -import { defineMessages, injectIntl } from 'react-intl'; - -import ScrollableList from 'soapbox/components/scrollable_list'; -import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account'; -import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag'; -import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; - -import Hashtag from '../../../components/hashtag'; -import { Tabs } from '../../../components/ui'; -import AccountContainer from '../../../containers/account_container'; -import StatusContainer from '../../../containers/status_container'; - -const messages = defineMessages({ - accounts: { id: 'search_results.accounts', defaultMessage: 'People' }, - statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' }, - hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' }, -}); - -export default @injectIntl -class SearchResults extends ImmutablePureComponent { - - static propTypes = { - value: PropTypes.string, - results: ImmutablePropTypes.map.isRequired, - submitted: PropTypes.bool, - expandSearch: PropTypes.func.isRequired, - selectedFilter: PropTypes.string.isRequired, - selectFilter: PropTypes.func.isRequired, - features: PropTypes.object.isRequired, - suggestions: ImmutablePropTypes.list, - trendingStatuses: ImmutablePropTypes.list, - trends: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - }; - - handleLoadMore = () => this.props.expandSearch(this.props.selectedFilter); - - handleSelectFilter = newActiveFilter => this.props.selectFilter(newActiveFilter); - - componentDidMount() { - this.props.fetchTrendingStatuses(); - } - - renderFilterBar() { - const { intl, selectedFilter } = this.props; - - const items = [ - { - text: intl.formatMessage(messages.accounts), - action: () => this.handleSelectFilter('accounts'), - name: 'accounts', - }, - { - text: intl.formatMessage(messages.statuses), - action: () => this.handleSelectFilter('statuses'), - name: 'statuses', - }, - { - text: intl.formatMessage(messages.hashtags), - action: () => this.handleSelectFilter('hashtags'), - name: 'hashtags', - }, - ]; - - return ; - } - - render() { - const { value, results, submitted, selectedFilter, suggestions, trendingStatuses, trends } = this.props; - - let searchResults; - let hasMore = false; - let loaded; - let noResultsMessage; - let placeholderComponent = PlaceholderStatus; - - if (selectedFilter === 'accounts') { - hasMore = results.get('accountsHasMore'); - loaded = results.get('accountsLoaded'); - placeholderComponent = PlaceholderAccount; - - if (results.get('accounts') && results.get('accounts').size > 0) { - searchResults = results.get('accounts').map(accountId => ); - } else if (!submitted && suggestions && !suggestions.isEmpty()) { - searchResults = suggestions.map(suggestion => ); - } else if (loaded) { - noResultsMessage = ( -
- -
- ); - } - } - - if (selectedFilter === 'statuses') { - hasMore = results.get('statusesHasMore'); - loaded = results.get('statusesLoaded'); - - if (results.get('statuses') && results.get('statuses').size > 0) { - searchResults = results.get('statuses').map(statusId => ); - } else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) { - searchResults = trendingStatuses.map(statusId => ); - } else if (loaded) { - noResultsMessage = ( -
- -
- ); - } - } - - if (selectedFilter === 'hashtags') { - hasMore = results.get('hashtagsHasMore'); - loaded = results.get('hashtagsLoaded'); - placeholderComponent = PlaceholderHashtag; - - if (results.get('hashtags') && results.get('hashtags').size > 0) { - searchResults = results.get('hashtags').map(hashtag => ); - } else if (!submitted && suggestions && !suggestions.isEmpty()) { - searchResults = trends.map(hashtag => ); - } else if (loaded) { - noResultsMessage = ( -
- -
- ); - } - } - - return ( - <> - {this.renderFilterBar()} - - {noResultsMessage || ( - - {searchResults} - - )} - - ); - } - -} diff --git a/app/soapbox/features/compose/components/search_results.tsx b/app/soapbox/features/compose/components/search_results.tsx new file mode 100644 index 000000000..1dc244920 --- /dev/null +++ b/app/soapbox/features/compose/components/search_results.tsx @@ -0,0 +1,187 @@ +import classNames from 'classnames'; +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; + +import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account'; +import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag'; +import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; +import { useAppDispatch } from 'soapbox/hooks'; + +import Hashtag from '../../../components/hashtag'; +import { Tabs } from '../../../components/ui'; +import AccountContainer from '../../../containers/account_container'; +import StatusContainer from '../../../containers/status_container'; + +import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; + +const messages = defineMessages({ + accounts: { id: 'search_results.accounts', defaultMessage: 'People' }, + statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' }, + hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' }, +}); + +type SearchFilter = 'accounts' | 'statuses' | 'hashtags'; + +interface ISearchResults { + value: string, + results: ImmutableMap, + submitted: boolean, + expandSearch: (filter: SearchFilter) => void, + selectedFilter: SearchFilter, + selectFilter: (filter: SearchFilter) => void, + suggestions: ImmutableList, + trendingStatuses: ImmutableList, + trends: ImmutableList, +} + +/** Displays search results depending on the active tab. */ +const SearchResults: React.FC = ({ + value, + results, + submitted, + expandSearch, + selectedFilter, + selectFilter, + suggestions, + trendingStatuses, + trends, +}) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const handleLoadMore = () => expandSearch(selectedFilter); + const handleSelectFilter = (newActiveFilter: SearchFilter) => selectFilter(newActiveFilter); + + useEffect(() => { + dispatch(fetchTrendingStatuses()); + }, []); + + const renderFilterBar = () => { + const items = [ + { + text: intl.formatMessage(messages.accounts), + action: () => handleSelectFilter('accounts'), + name: 'accounts', + }, + { + text: intl.formatMessage(messages.statuses), + action: () => handleSelectFilter('statuses'), + name: 'statuses', + }, + { + text: intl.formatMessage(messages.hashtags), + action: () => handleSelectFilter('hashtags'), + name: 'hashtags', + }, + ]; + + return ; + }; + + let searchResults; + let hasMore = false; + let loaded; + let noResultsMessage; + let placeholderComponent: React.ComponentType = PlaceholderStatus; + + if (selectedFilter === 'accounts') { + hasMore = results.get('accountsHasMore'); + loaded = results.get('accountsLoaded'); + placeholderComponent = PlaceholderAccount; + + if (results.get('accounts') && results.get('accounts').size > 0) { + searchResults = results.get('accounts').map((accountId: string) => ); + } else if (!submitted && suggestions && !suggestions.isEmpty()) { + searchResults = suggestions.map(suggestion => ); + } else if (loaded) { + noResultsMessage = ( +
+ +
+ ); + } + } + + if (selectedFilter === 'statuses') { + hasMore = results.get('statusesHasMore'); + loaded = results.get('statusesLoaded'); + + if (results.get('statuses') && results.get('statuses').size > 0) { + searchResults = results.get('statuses').map((statusId: string) => ( + // @ts-ignore + + )); + } else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) { + searchResults = trendingStatuses.map(statusId => ( + // @ts-ignore + + )); + } else if (loaded) { + noResultsMessage = ( +
+ +
+ ); + } + } + + if (selectedFilter === 'hashtags') { + hasMore = results.get('hashtagsHasMore'); + loaded = results.get('hashtagsLoaded'); + placeholderComponent = PlaceholderHashtag; + + if (results.get('hashtags') && results.get('hashtags').size > 0) { + searchResults = results.get('hashtags').map((hashtag: ImmutableMap) => ); + } else if (!submitted && suggestions && !suggestions.isEmpty()) { + searchResults = trends.map(hashtag => ); + } else if (loaded) { + noResultsMessage = ( +
+ +
+ ); + } + } + + return ( + <> + {renderFilterBar()} + + {noResultsMessage || ( + + {searchResults} + + )} + + ); +}; + +export default SearchResults; From 5bbfb0cc398e4e076c0458b42f71f3e168e876ee Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 14 May 2022 13:07:32 -0500 Subject: [PATCH 2/6] Convert Trends, Suggestions, and TrendingStatuses reducers to TypeScript --- .../compose/components/search_results.tsx | 32 ++++++------ .../follow_recommendations_list.tsx | 4 +- .../{suggestions.js => suggestions.ts} | 50 ++++++++++++++----- app/soapbox/reducers/trending_statuses.js | 31 ------------ app/soapbox/reducers/trending_statuses.ts | 37 ++++++++++++++ app/soapbox/reducers/{trends.js => trends.ts} | 16 ++++-- 6 files changed, 105 insertions(+), 65 deletions(-) rename app/soapbox/reducers/{suggestions.js => suggestions.ts} (54%) delete mode 100644 app/soapbox/reducers/trending_statuses.js create mode 100644 app/soapbox/reducers/trending_statuses.ts rename app/soapbox/reducers/{trends.js => trends.ts} (53%) diff --git a/app/soapbox/features/compose/components/search_results.tsx b/app/soapbox/features/compose/components/search_results.tsx index 1dc244920..d24c7631d 100644 --- a/app/soapbox/features/compose/components/search_results.tsx +++ b/app/soapbox/features/compose/components/search_results.tsx @@ -3,19 +3,23 @@ import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl'; +import { expandSearch, setFilter } from 'soapbox/actions/search'; import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses'; import ScrollableList from 'soapbox/components/scrollable_list'; import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account'; import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import Hashtag from '../../../components/hashtag'; import { Tabs } from '../../../components/ui'; import AccountContainer from '../../../containers/account_container'; import StatusContainer from '../../../containers/status_container'; -import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import type { + Map as ImmutableMap, + List as ImmutableList, +} from 'immutable'; const messages = defineMessages({ accounts: { id: 'search_results.accounts', defaultMessage: 'People' }, @@ -38,22 +42,20 @@ interface ISearchResults { } /** Displays search results depending on the active tab. */ -const SearchResults: React.FC = ({ - value, - results, - submitted, - expandSearch, - selectedFilter, - selectFilter, - suggestions, - trendingStatuses, - trends, -}) => { +const SearchResults: React.FC = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const handleLoadMore = () => expandSearch(selectedFilter); - const handleSelectFilter = (newActiveFilter: SearchFilter) => selectFilter(newActiveFilter); + const value = useAppSelector(state => state.search.get('submittedValue')); + const results = useAppSelector(state => state.search.get('results')); + const suggestions = useAppSelector(state => state.suggestions.items); + const trendingStatuses = useAppSelector(state => state.trending_statuses.items); + const trends = useAppSelector(state => state.trends.items); + const submitted = useAppSelector(state => state.search.get('submitted')); + const selectedFilter = useAppSelector(state => state.search.get('filter')); + + const handleLoadMore = () => dispatch(expandSearch(selectedFilter)); + const handleSelectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter)); useEffect(() => { dispatch(fetchTrendingStatuses()); diff --git a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx index 4ca838072..109febdf4 100644 --- a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx +++ b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx @@ -31,8 +31,8 @@ const FollowRecommendationsList: React.FC = () => { return (
- {suggestions.size > 0 ? suggestions.map((suggestion: { account: string }) => ( - + {suggestions.size > 0 ? suggestions.map(suggestion => ( + )) : (
diff --git a/app/soapbox/reducers/suggestions.js b/app/soapbox/reducers/suggestions.ts similarity index 54% rename from app/soapbox/reducers/suggestions.js rename to app/soapbox/reducers/suggestions.ts index ac00eecf1..bca64c9e2 100644 --- a/app/soapbox/reducers/suggestions.js +++ b/app/soapbox/reducers/suggestions.ts @@ -1,4 +1,8 @@ -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { + Map as ImmutableMap, + List as ImmutableList, + Record as ImmutableRecord, +} from 'immutable'; import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts'; import { DOMAIN_BLOCK_SUCCESS } from 'soapbox/actions/domain_blocks'; @@ -13,42 +17,64 @@ import { SUGGESTIONS_V2_FETCH_FAIL, } from '../actions/suggestions'; -const initialState = ImmutableMap({ - items: ImmutableList(), +import type { AnyAction } from 'redux'; + +type SuggestionSource = 'past_interactions' | 'staff' | 'global'; + +type ReducerSuggestion = { + source: SuggestionSource, + account: string, +} + +type SuggestionAccount = { + id: string, +} + +type Suggestion = { + source: SuggestionSource, + account: SuggestionAccount, +} + +const ReducerRecord = ImmutableRecord({ + items: ImmutableList>(), isLoading: false, }); -// Convert a v1 account into a v2 suggestion -const accountToSuggestion = account => { +type State = ReturnType; + +/** Convert a v1 account into a v2 suggestion. */ +const accountToSuggestion = (account: SuggestionAccount): ReducerSuggestion => { return { source: 'past_interactions', account: account.id, }; }; -const importAccounts = (state, accounts) => { +/** Import plain accounts into the reducer (legacy). */ +const importAccounts = (state: State, accounts: SuggestionAccount[]): State => { return state.withMutations(state => { - state.set('items', fromJS(accounts.map(accountToSuggestion))); + state.set('items', ImmutableList(accounts.map(account => ImmutableMap(accountToSuggestion(account))))); state.set('isLoading', false); }); }; -const importSuggestions = (state, suggestions) => { +/** Import full suggestion objects. */ +const importSuggestions = (state: State, suggestions: Suggestion[]): State => { return state.withMutations(state => { - state.set('items', fromJS(suggestions.map(x => ({ ...x, account: x.account.id })))); + state.set('items', ImmutableList(suggestions.map(x => ImmutableMap({ ...x, account: x.account.id })))); state.set('isLoading', false); }); }; -const dismissAccount = (state, accountId) => { +const dismissAccount = (state: State, accountId: string): State => { return state.update('items', items => items.filterNot(item => item.get('account') === accountId)); }; -const dismissAccounts = (state, accountIds) => { +const dismissAccounts = (state: State, accountIds: string[]): State => { return state.update('items', items => items.filterNot(item => accountIds.includes(item.get('account')))); }; -export default function suggestionsReducer(state = initialState, action) { +export default function suggestionsReducer(state = ReducerRecord(), action: AnyAction) { switch (action.type) { case SUGGESTIONS_FETCH_REQUEST: case SUGGESTIONS_V2_FETCH_REQUEST: diff --git a/app/soapbox/reducers/trending_statuses.js b/app/soapbox/reducers/trending_statuses.js deleted file mode 100644 index ee4fa234b..000000000 --- a/app/soapbox/reducers/trending_statuses.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; - -import { - TRENDING_STATUSES_FETCH_REQUEST, - TRENDING_STATUSES_FETCH_SUCCESS, -} from 'soapbox/actions/trending_statuses'; - -const initialState = ImmutableMap({ - items: ImmutableOrderedSet(), - isLoading: false, -}); - -const toIds = items => ImmutableOrderedSet(items.map(item => item.id)); - -const importStatuses = (state, statuses) => { - return state.withMutations(state => { - state.set('items', toIds(statuses)); - state.set('isLoading', false); - }); -}; - -export default function trending_statuses(state = initialState, action) { - switch (action.type) { - case TRENDING_STATUSES_FETCH_REQUEST: - return state.set('isLoading', true); - case TRENDING_STATUSES_FETCH_SUCCESS: - return importStatuses(state, action.statuses); - default: - return state; - } -} diff --git a/app/soapbox/reducers/trending_statuses.ts b/app/soapbox/reducers/trending_statuses.ts new file mode 100644 index 000000000..afaf5d9af --- /dev/null +++ b/app/soapbox/reducers/trending_statuses.ts @@ -0,0 +1,37 @@ +import { Record as ImmutableRecord, OrderedSet as ImmutableOrderedSet } from 'immutable'; + +import { + TRENDING_STATUSES_FETCH_REQUEST, + TRENDING_STATUSES_FETCH_SUCCESS, +} from 'soapbox/actions/trending_statuses'; + +import type { AnyAction } from 'redux'; + +const ReducerRecord = ImmutableRecord({ + items: ImmutableOrderedSet(), + isLoading: false, +}); + +type State = ReturnType; + +type IdEntity = { id: string }; + +const toIds = (items: IdEntity[]) => ImmutableOrderedSet(items.map(item => item.id)); + +const importStatuses = (state: State, statuses: IdEntity[]): State => { + return state.withMutations(state => { + state.set('items', toIds(statuses)); + state.set('isLoading', false); + }); +}; + +export default function trending_statuses(state = ReducerRecord(), action: AnyAction) { + switch (action.type) { + case TRENDING_STATUSES_FETCH_REQUEST: + return state.set('isLoading', true); + case TRENDING_STATUSES_FETCH_SUCCESS: + return importStatuses(state, action.statuses); + default: + return state; + } +} diff --git a/app/soapbox/reducers/trends.js b/app/soapbox/reducers/trends.ts similarity index 53% rename from app/soapbox/reducers/trends.js rename to app/soapbox/reducers/trends.ts index eb7df59bf..cb061c7b7 100644 --- a/app/soapbox/reducers/trends.js +++ b/app/soapbox/reducers/trends.ts @@ -1,4 +1,8 @@ -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { + Map as ImmutableMap, + Record as ImmutableRecord, + List as ImmutableList, +} from 'immutable'; import { TRENDS_FETCH_REQUEST, @@ -6,18 +10,20 @@ import { TRENDS_FETCH_FAIL, } from '../actions/trends'; -const initialState = ImmutableMap({ - items: ImmutableList(), +import type { AnyAction } from 'redux'; + +const ReducerRecord = ImmutableRecord({ + items: ImmutableList>(), isLoading: false, }); -export default function trendsReducer(state = initialState, action) { +export default function trendsReducer(state = ReducerRecord(), action: AnyAction) { switch (action.type) { case TRENDS_FETCH_REQUEST: return state.set('isLoading', true); case TRENDS_FETCH_SUCCESS: return state.withMutations(map => { - map.set('items', fromJS(action.tags.map((x => x)))); + map.set('items', ImmutableList(action.tags.map(ImmutableMap))); map.set('isLoading', false); }); case TRENDS_FETCH_FAIL: From 88404f32b6526329b2d8f492f0ac8f7c1842811e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 14 May 2022 13:18:31 -0500 Subject: [PATCH 3/6] Search reducer: convert to TypeScript --- .../compose/components/search_results.tsx | 8 ++-- app/soapbox/reducers/{search.js => search.ts} | 43 ++++++++++++++----- 2 files changed, 36 insertions(+), 15 deletions(-) rename app/soapbox/reducers/{search.js => search.ts} (69%) diff --git a/app/soapbox/features/compose/components/search_results.tsx b/app/soapbox/features/compose/components/search_results.tsx index d24c7631d..d3fa555fa 100644 --- a/app/soapbox/features/compose/components/search_results.tsx +++ b/app/soapbox/features/compose/components/search_results.tsx @@ -46,13 +46,13 @@ const SearchResults: React.FC = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const value = useAppSelector(state => state.search.get('submittedValue')); - const results = useAppSelector(state => state.search.get('results')); + const value = useAppSelector(state => state.search.submittedValue); + const results = useAppSelector(state => state.search.results); const suggestions = useAppSelector(state => state.suggestions.items); const trendingStatuses = useAppSelector(state => state.trending_statuses.items); const trends = useAppSelector(state => state.trends.items); - const submitted = useAppSelector(state => state.search.get('submitted')); - const selectedFilter = useAppSelector(state => state.search.get('filter')); + const submitted = useAppSelector(state => state.search.submitted); + const selectedFilter = useAppSelector(state => state.search.filter); const handleLoadMore = () => dispatch(expandSearch(selectedFilter)); const handleSelectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter)); diff --git a/app/soapbox/reducers/search.js b/app/soapbox/reducers/search.ts similarity index 69% rename from app/soapbox/reducers/search.js rename to app/soapbox/reducers/search.ts index e086ef4a0..f315cbb90 100644 --- a/app/soapbox/reducers/search.js +++ b/app/soapbox/reducers/search.ts @@ -1,4 +1,10 @@ -import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; +import { + Map as ImmutableMap, + Record as ImmutableRecord, + List as ImmutableList, + OrderedSet as ImmutableOrderedSet, + fromJS, +} from 'immutable'; import { COMPOSE_MENTION, @@ -17,26 +23,39 @@ import { SEARCH_EXPAND_SUCCESS, } from '../actions/search'; -const initialState = ImmutableMap({ +import type { AnyAction } from 'redux'; + +const ReducerRecord = ImmutableRecord({ value: '', submitted: false, submittedValue: '', hidden: false, - results: ImmutableMap(), + results: ImmutableMap(), filter: 'accounts', }); -const toIds = items => { +type State = ReturnType; + +type IdEntity = { id: string }; +type SearchType = 'accounts' | 'statuses' | 'hashtags'; + +type Results = { + accounts: IdEntity[], + statuses: IdEntity[], + hashtags: Record[], +} + +const toIds = (items: IdEntity[]) => { return ImmutableOrderedSet(items.map(item => item.id)); }; -const importResults = (state, results, searchTerm, searchType) => { +const importResults = (state: State, results: Results, searchTerm: string, searchType: SearchType): State => { return state.withMutations(state => { if (state.get('value') === searchTerm && state.get('filter') === searchType) { state.set('results', ImmutableMap({ accounts: toIds(results.accounts), statuses: toIds(results.statuses), - hashtags: fromJS(results.hashtags), // it's a list of maps + hashtags: ImmutableList(results.hashtags.map(ImmutableMap)), // it's a list of maps accountsHasMore: results.accounts.length >= 20, statusesHasMore: results.statuses.length >= 20, hashtagsHasMore: results.hashtags.length >= 20, @@ -50,17 +69,19 @@ const importResults = (state, results, searchTerm, searchType) => { }); }; -const paginateResults = (state, searchType, results, searchTerm) => { +const paginateResults = (state: State, searchType: SearchType, results: Results, searchTerm: string): State => { return state.withMutations(state => { - if (state.get('value') === searchTerm) { + if (state.value === searchTerm) { state.setIn(['results', `${searchType}HasMore`], results[searchType].length >= 20); state.setIn(['results', `${searchType}Loaded`], true); state.updateIn(['results', searchType], items => { const data = results[searchType]; // Hashtags are a list of maps. Others are IDs. if (searchType === 'hashtags') { + // @ts-ignore return items.concat(fromJS(data)); } else { + // @ts-ignore return items.concat(toIds(data)); } }); @@ -68,7 +89,7 @@ const paginateResults = (state, searchType, results, searchTerm) => { }); }; -const handleSubmitted = (state, value) => { +const handleSubmitted = (state: State, value: string): State => { return state.withMutations(state => { state.set('results', ImmutableMap()); state.set('submitted', true); @@ -76,12 +97,12 @@ const handleSubmitted = (state, value) => { }); }; -export default function search(state = initialState, action) { +export default function search(state = ReducerRecord(), action: AnyAction) { switch (action.type) { case SEARCH_CHANGE: return state.set('value', action.value); case SEARCH_CLEAR: - return initialState; + return ReducerRecord(); case SEARCH_SHOW: return state.set('hidden', false); case COMPOSE_REPLY: From 640a6ab8eb00785123e9a734c8ded0f59e8acabd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 14 May 2022 13:20:21 -0500 Subject: [PATCH 4/6] Delete SearchResultsContainer --- .../compose/components/search_results.tsx | 14 +------- .../containers/search_results_container.js | 33 ------------------- app/soapbox/features/search/index.tsx | 4 +-- 3 files changed, 3 insertions(+), 48 deletions(-) delete mode 100644 app/soapbox/features/compose/containers/search_results_container.js diff --git a/app/soapbox/features/compose/components/search_results.tsx b/app/soapbox/features/compose/components/search_results.tsx index d3fa555fa..2543e3626 100644 --- a/app/soapbox/features/compose/components/search_results.tsx +++ b/app/soapbox/features/compose/components/search_results.tsx @@ -29,20 +29,8 @@ const messages = defineMessages({ type SearchFilter = 'accounts' | 'statuses' | 'hashtags'; -interface ISearchResults { - value: string, - results: ImmutableMap, - submitted: boolean, - expandSearch: (filter: SearchFilter) => void, - selectedFilter: SearchFilter, - selectFilter: (filter: SearchFilter) => void, - suggestions: ImmutableList, - trendingStatuses: ImmutableList, - trends: ImmutableList, -} - /** Displays search results depending on the active tab. */ -const SearchResults: React.FC = () => { +const SearchResults: React.FC = () => { const intl = useIntl(); const dispatch = useAppDispatch(); diff --git a/app/soapbox/features/compose/containers/search_results_container.js b/app/soapbox/features/compose/containers/search_results_container.js deleted file mode 100644 index 4ab9b712f..000000000 --- a/app/soapbox/features/compose/containers/search_results_container.js +++ /dev/null @@ -1,33 +0,0 @@ -import { connect } from 'react-redux'; - -import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses'; -import { getFeatures } from 'soapbox/utils/features'; - -import { expandSearch, setFilter } from '../../../actions/search'; -import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions'; -import SearchResults from '../components/search_results'; - -const mapStateToProps = state => { - const instance = state.get('instance'); - - return { - value: state.getIn(['search', 'submittedValue']), - results: state.getIn(['search', 'results']), - suggestions: state.getIn(['suggestions', 'items']), - trendingStatuses: state.getIn(['trending_statuses', 'items']), - trends: state.getIn(['trends', 'items']), - submitted: state.getIn(['search', 'submitted']), - selectedFilter: state.getIn(['search', 'filter']), - features: getFeatures(instance), - }; -}; - -const mapDispatchToProps = dispatch => ({ - fetchSuggestions: () => dispatch(fetchSuggestions()), - fetchTrendingStatuses: () => dispatch(fetchTrendingStatuses()), - expandSearch: type => dispatch(expandSearch(type)), - dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))), - selectFilter: newActiveFilter => dispatch(setFilter(newActiveFilter)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(SearchResults); diff --git a/app/soapbox/features/search/index.tsx b/app/soapbox/features/search/index.tsx index 3fdf625f6..c17691c37 100644 --- a/app/soapbox/features/search/index.tsx +++ b/app/soapbox/features/search/index.tsx @@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl'; import { Column } from 'soapbox/components/ui'; import Search from 'soapbox/features/compose/components/search'; -import SearchResultsContainer from 'soapbox/features/compose/containers/search_results_container'; +import SearchResults from 'soapbox/features/compose/components/search_results'; const messages = defineMessages({ heading: { id: 'column.search', defaultMessage: 'Search' }, @@ -16,7 +16,7 @@ const SearchPage = () => {
- +
); From 295eb40f14cb205a02e82b84cd9f7fc1efcd749a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 14 May 2022 13:23:37 -0500 Subject: [PATCH 5/6] SearchResults: remove unused import --- app/soapbox/features/compose/components/search_results.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/soapbox/features/compose/components/search_results.tsx b/app/soapbox/features/compose/components/search_results.tsx index 2543e3626..66d8f2062 100644 --- a/app/soapbox/features/compose/components/search_results.tsx +++ b/app/soapbox/features/compose/components/search_results.tsx @@ -16,10 +16,7 @@ import { Tabs } from '../../../components/ui'; import AccountContainer from '../../../containers/account_container'; import StatusContainer from '../../../containers/status_container'; -import type { - Map as ImmutableMap, - List as ImmutableList, -} from 'immutable'; +import type { Map as ImmutableMap } from 'immutable'; const messages = defineMessages({ accounts: { id: 'search_results.accounts', defaultMessage: 'People' }, From 2e192153ed6b3413d388cf0bc9b1d3e1e9d5beb0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 14 May 2022 14:13:58 -0500 Subject: [PATCH 6/6] Fix reducer tests --- app/soapbox/reducers/__tests__/search-test.js | 14 +++++++------- app/soapbox/reducers/__tests__/suggestions-test.js | 13 ++++++++----- app/soapbox/reducers/__tests__/trends-test.js | 10 +++++----- app/soapbox/reducers/search.ts | 2 +- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/app/soapbox/reducers/__tests__/search-test.js b/app/soapbox/reducers/__tests__/search-test.js index b1138b663..c3b8d1ea0 100644 --- a/app/soapbox/reducers/__tests__/search-test.js +++ b/app/soapbox/reducers/__tests__/search-test.js @@ -6,11 +6,11 @@ import { SEARCH_EXPAND_SUCCESS, } from 'soapbox/actions/search'; -import reducer from '../search'; +import reducer, { ReducerRecord } from '../search'; describe('search reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap({ + expect(reducer(undefined, {})).toEqual(ReducerRecord({ value: '', submitted: false, submittedValue: '', @@ -22,7 +22,7 @@ describe('search reducer', () => { describe('SEARCH_CHANGE', () => { it('sets the value', () => { - const state = ImmutableMap({ value: 'hell' }); + const state = ReducerRecord({ value: 'hell' }); const action = { type: SEARCH_CHANGE, value: 'hello' }; expect(reducer(state, action).get('value')).toEqual('hello'); }); @@ -30,7 +30,7 @@ describe('search reducer', () => { describe('SEARCH_CLEAR', () => { it('resets the state', () => { - const state = ImmutableMap({ + const state = ReducerRecord({ value: 'hello world', submitted: true, submittedValue: 'hello world', @@ -41,7 +41,7 @@ describe('search reducer', () => { const action = { type: SEARCH_CLEAR }; - const expected = ImmutableMap({ + const expected = ReducerRecord({ value: '', submitted: false, submittedValue: '', @@ -56,7 +56,7 @@ describe('search reducer', () => { describe(SEARCH_EXPAND_SUCCESS, () => { it('imports hashtags as maps', () => { - const state = ImmutableMap({ + const state = ReducerRecord({ value: 'artist', submitted: true, submittedValue: 'artist', @@ -82,7 +82,7 @@ describe('search reducer', () => { searchType: 'hashtags', }; - const expected = ImmutableMap({ + const expected = ReducerRecord({ value: 'artist', submitted: true, submittedValue: 'artist', diff --git a/app/soapbox/reducers/__tests__/suggestions-test.js b/app/soapbox/reducers/__tests__/suggestions-test.js index 7da0b7f75..3a11b5b56 100644 --- a/app/soapbox/reducers/__tests__/suggestions-test.js +++ b/app/soapbox/reducers/__tests__/suggestions-test.js @@ -1,4 +1,7 @@ -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { + Record as ImmutableRecord, + fromJS, +} from 'immutable'; import { SUGGESTIONS_DISMISS } from 'soapbox/actions/suggestions'; @@ -6,10 +9,10 @@ import reducer from '../suggestions'; describe('suggestions reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap({ - items: ImmutableList(), - isLoading: false, - })); + const result = reducer(undefined, {}); + expect(ImmutableRecord.isRecord(result)).toBe(true); + expect(result.items.isEmpty()).toBe(true); + expect(result.isLoading).toBe(false); }); describe('SUGGESTIONS_DISMISS', () => { diff --git a/app/soapbox/reducers/__tests__/trends-test.js b/app/soapbox/reducers/__tests__/trends-test.js index 5ebeabb31..22d7d34e4 100644 --- a/app/soapbox/reducers/__tests__/trends-test.js +++ b/app/soapbox/reducers/__tests__/trends-test.js @@ -1,12 +1,12 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Record as ImmutableRecord } from 'immutable'; import reducer from '../trends'; describe('trends reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap({ - items: ImmutableList(), - isLoading: false, - })); + const result = reducer(undefined, {}); + expect(ImmutableRecord.isRecord(result)).toBe(true); + expect(result.items.isEmpty()).toBe(true); + expect(result.isLoading).toBe(false); }); }); diff --git a/app/soapbox/reducers/search.ts b/app/soapbox/reducers/search.ts index f315cbb90..64d2a6a8c 100644 --- a/app/soapbox/reducers/search.ts +++ b/app/soapbox/reducers/search.ts @@ -25,7 +25,7 @@ import { import type { AnyAction } from 'redux'; -const ReducerRecord = ImmutableRecord({ +export const ReducerRecord = ImmutableRecord({ value: '', submitted: false, submittedValue: '',