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..66d8f2062 --- /dev/null +++ b/app/soapbox/features/compose/components/search_results.tsx @@ -0,0 +1,174 @@ +import classNames from 'classnames'; +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 { 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 } 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'; + +/** Displays search results depending on the active tab. */ +const SearchResults: React.FC = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + 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.submitted); + const selectedFilter = useAppSelector(state => state.search.filter); + + const handleLoadMore = () => dispatch(expandSearch(selectedFilter)); + const handleSelectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(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; 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/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/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 = () => {
- +
); 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.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..64d2a6a8c 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'; + +export 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: 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: