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 05cbe26639..0000000000 Binary files a/app/soapbox/features/compose/components/search_results.js and /dev/null differ 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 0000000000..66d8f2062f --- /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 4ab9b712fc..0000000000 Binary files a/app/soapbox/features/compose/containers/search_results_container.js and /dev/null differ 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 4ca8380722..109febdf45 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 3fdf625f68..c17691c37f 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 b1138b6631..c3b8d1ea0c 100644 Binary files a/app/soapbox/reducers/__tests__/search-test.js and b/app/soapbox/reducers/__tests__/search-test.js differ diff --git a/app/soapbox/reducers/__tests__/suggestions-test.js b/app/soapbox/reducers/__tests__/suggestions-test.js index 7da0b7f750..3a11b5b56a 100644 Binary files a/app/soapbox/reducers/__tests__/suggestions-test.js and b/app/soapbox/reducers/__tests__/suggestions-test.js differ diff --git a/app/soapbox/reducers/__tests__/trends-test.js b/app/soapbox/reducers/__tests__/trends-test.js index 5ebeabb31d..22d7d34e4f 100644 Binary files a/app/soapbox/reducers/__tests__/trends-test.js and b/app/soapbox/reducers/__tests__/trends-test.js differ 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 e086ef4a0c..64d2a6a8c2 100644 Binary files a/app/soapbox/reducers/search.js and b/app/soapbox/reducers/search.ts differ 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 ac00eecf11..bca64c9e27 100644 Binary files a/app/soapbox/reducers/suggestions.js and b/app/soapbox/reducers/suggestions.ts differ diff --git a/app/soapbox/reducers/trending_statuses.js b/app/soapbox/reducers/trending_statuses.js deleted file mode 100644 index ee4fa234b7..0000000000 Binary files a/app/soapbox/reducers/trending_statuses.js and /dev/null differ diff --git a/app/soapbox/reducers/trending_statuses.ts b/app/soapbox/reducers/trending_statuses.ts new file mode 100644 index 0000000000..afaf5d9af5 --- /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 eb7df59bfa..cb061c7b7e 100644 Binary files a/app/soapbox/reducers/trends.js and b/app/soapbox/reducers/trends.ts differ