From f148cda74a8adce6a127f499913c0401863ef10f Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 1 Jul 2022 16:07:01 -0400 Subject: [PATCH 01/12] Extend Account component --- app/soapbox/components/account.tsx | 66 +++++++++++-------- app/soapbox/components/quoted-status.tsx | 1 + app/soapbox/components/scroll-top-button.tsx | 12 ++-- app/soapbox/components/sidebar_menu.tsx | 6 +- app/soapbox/components/status.tsx | 7 +- app/soapbox/components/ui/stack/stack.tsx | 2 +- .../compose/components/reply_indicator.tsx | 1 + .../steps/suggested-accounts-step.tsx | 1 + .../features/ui/components/actions_modal.tsx | 1 + .../modals/report-modal/report-modal.tsx | 1 + .../ui/components/profile-dropdown.tsx | 2 +- 11 files changed, 60 insertions(+), 40 deletions(-) diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 2d53e6da8..72ba40e38 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -9,7 +9,7 @@ import { getAcct } from 'soapbox/utils/accounts'; import { displayFqn } from 'soapbox/utils/state'; import RelativeTimestamp from './relative_timestamp'; -import { Avatar, Emoji, HStack, Icon, IconButton, Text } from './ui'; +import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui'; import type { Account as AccountEntity } from 'soapbox/types/entities'; @@ -57,7 +57,9 @@ interface IAccount { timestamp?: string | Date, timestampUrl?: string, futureTimestamp?: boolean, + withAccountNote?: boolean, withDate?: boolean, + withLinkToProfile?: boolean, withRelationship?: boolean, showEdit?: boolean, emoji?: string, @@ -78,7 +80,9 @@ const Account = ({ timestamp, timestampUrl, futureTimestamp = false, + withAccountNote = false, withDate = false, + withLinkToProfile = true, withRelationship = true, showEdit = false, emoji, @@ -154,12 +158,12 @@ const Account = ({ if (withDate) timestamp = account.created_at; - const LinkEl: any = showProfileHoverCard ? Link : 'div'; + const LinkEl: any = withLinkToProfile ? Link : 'div'; return (
- + {children}} @@ -202,35 +206,45 @@ const Account = ({ - - @{username} + + + @{username} - {account.favicon && ( - - )} + {account.favicon && ( + + )} - {(timestamp) ? ( - <> - · + {(timestamp) ? ( + <> + · - {timestampUrl ? ( - + {timestampUrl ? ( + + + + ) : ( - - ) : ( - - )} - - ) : null} + )} + + ) : null} - {showEdit ? ( - <> - · + {showEdit ? ( + <> + · - - - ) : null} - + + + ) : null} + + + {withAccountNote && ( + + )} +
diff --git a/app/soapbox/components/quoted-status.tsx b/app/soapbox/components/quoted-status.tsx index 5d6eb526e..82fe8860a 100644 --- a/app/soapbox/components/quoted-status.tsx +++ b/app/soapbox/components/quoted-status.tsx @@ -137,6 +137,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => timestamp={status.created_at} withRelationship={false} showProfileHoverCard={!compose} + withLinkToProfile={false} /> {renderReplyMentions()} diff --git a/app/soapbox/components/scroll-top-button.tsx b/app/soapbox/components/scroll-top-button.tsx index 3652296ef..8735e4296 100644 --- a/app/soapbox/components/scroll-top-button.tsx +++ b/app/soapbox/components/scroll-top-button.tsx @@ -34,6 +34,12 @@ const ScrollTopButton: React.FC = ({ const [scrolled, setScrolled] = useState(false); const autoload = settings.get('autoloadTimelines') === true; + const visible = count > 0 && scrolled; + + const classes = classNames('left-1/2 -translate-x-1/2 fixed top-20 z-50', { + 'hidden': !visible, + }); + const getScrollTop = (): number => { return (document.scrollingElement || document.documentElement).scrollTop; }; @@ -75,12 +81,6 @@ const ScrollTopButton: React.FC = ({ maybeUnload(); }, [count]); - const visible = count > 0 && scrolled; - - const classes = classNames('left-1/2 -translate-x-1/2 fixed top-20 z-50', { - 'hidden': !visible, - }); - return (
diff --git a/app/soapbox/components/sidebar_menu.tsx b/app/soapbox/components/sidebar_menu.tsx index 2940c5bfa..3164c83e3 100644 --- a/app/soapbox/components/sidebar_menu.tsx +++ b/app/soapbox/components/sidebar_menu.tsx @@ -84,7 +84,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { const getAccount = makeGetAccount(); const instance = useAppSelector((state) => state.instance); const me = useAppSelector((state) => state.me); - const account = useAppSelector((state) => me ? getAccount(state, me) : null); + const account = useAppSelector((state) => me ? getAccount(state, me) : null); const otherAccounts: ImmutableList = useAppSelector((state) => getOtherAccounts(state)); const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen); const settings = useAppSelector((state) => getSettings(state)); @@ -121,7 +121,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { const renderAccount = (account: AccountEntity) => (
- +
); @@ -166,7 +166,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { - + diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 200b2de10..52ec1a93b 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -134,11 +134,11 @@ class Status extends ImmutablePureComponent { this.didShowCard = Boolean(!this.props.muted && !this.props.hidden && this.props.status && this.props.status.card); } - getSnapshotBeforeUpdate(): ScrollPosition | undefined { + getSnapshotBeforeUpdate(): ScrollPosition | null { if (this.props.getScrollPosition) { - return this.props.getScrollPosition(); + return this.props.getScrollPosition() || null; } else { - return undefined; + return null; } } @@ -483,6 +483,7 @@ class Status extends ImmutablePureComponent { hideActions={!reblogElement} showEdit={!!status.edited_at} showProfileHoverCard={this.props.hoverable} + withLinkToProfile={this.props.hoverable} />
diff --git a/app/soapbox/components/ui/stack/stack.tsx b/app/soapbox/components/ui/stack/stack.tsx index 3bb96d276..17b4df36e 100644 --- a/app/soapbox/components/ui/stack/stack.tsx +++ b/app/soapbox/components/ui/stack/stack.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import React from 'react'; -type SIZES = 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 10 +type SIZES = 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 10 const spaces = { '0.5': 'space-y-0.5', diff --git a/app/soapbox/features/compose/components/reply_indicator.tsx b/app/soapbox/features/compose/components/reply_indicator.tsx index f47b0494b..99eb5a43f 100644 --- a/app/soapbox/features/compose/components/reply_indicator.tsx +++ b/app/soapbox/features/compose/components/reply_indicator.tsx @@ -39,6 +39,7 @@ const ReplyIndicator: React.FC = ({ status, hideActions, onCanc id={status.getIn(['account', 'id']) as string} timestamp={status.created_at} showProfileHoverCard={false} + withLinkToProfile={false} /> void }) => { // @ts-ignore: TS thinks `id` is passed to , but it isn't id={suggestion.account} showProfileHoverCard={false} + withLinkToProfile={false} /> ))} diff --git a/app/soapbox/features/ui/components/actions_modal.tsx b/app/soapbox/features/ui/components/actions_modal.tsx index e123149b6..e760df031 100644 --- a/app/soapbox/features/ui/components/actions_modal.tsx +++ b/app/soapbox/features/ui/components/actions_modal.tsx @@ -60,6 +60,7 @@ const ActionsModal: React.FC = ({ status, actions, onClick, onClo key={status.account as string} id={status.account as string} showProfileHoverCard={false} + withLinkToProfile={false} timestamp={status.created_at} /> diff --git a/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx index 39575beb4..cd4f1ff08 100644 --- a/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx +++ b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx @@ -49,6 +49,7 @@ const SelectedStatus = ({ statusId }: { statusId: string }) => { diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index 0216f3d39..65bf945bf 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -58,7 +58,7 @@ const ProfileDropdown: React.FC = ({ account, children }) => { const renderAccount = (account: AccountEntity) => { return ( - + ); }; From 1309521b9cdc8dcc20aaf81b56b362416a65d08f Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 1 Jul 2022 16:07:16 -0400 Subject: [PATCH 02/12] Improve Stack and HStack components --- app/soapbox/components/ui/hstack/hstack.tsx | 3 ++- app/soapbox/components/ui/stack/stack.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/soapbox/components/ui/hstack/hstack.tsx b/app/soapbox/components/ui/hstack/hstack.tsx index 9769ebc60..2a021d903 100644 --- a/app/soapbox/components/ui/hstack/hstack.tsx +++ b/app/soapbox/components/ui/hstack/hstack.tsx @@ -6,6 +6,7 @@ const justifyContentOptions = { center: 'justify-center', start: 'justify-start', end: 'justify-end', + around: 'justify-around', }; const alignItemsOptions = { @@ -32,7 +33,7 @@ interface IHStack { /** Extra class names on the
element. */ className?: string, /** Horizontal alignment of children. */ - justifyContent?: 'between' | 'center' | 'start' | 'end', + justifyContent?: 'between' | 'center' | 'start' | 'end' | 'around', /** Size of the gap between elements. */ space?: 0.5 | 1 | 1.5 | 2 | 3 | 4 | 6 | 8, /** Whether to let the flexbox grow. */ diff --git a/app/soapbox/components/ui/stack/stack.tsx b/app/soapbox/components/ui/stack/stack.tsx index 17b4df36e..9ecb4a104 100644 --- a/app/soapbox/components/ui/stack/stack.tsx +++ b/app/soapbox/components/ui/stack/stack.tsx @@ -4,6 +4,7 @@ import React from 'react'; type SIZES = 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 10 const spaces = { + 0: 'space-y-0', '0.5': 'space-y-0.5', 1: 'space-y-1', '1.5': 'space-y-1.5', From da98a1e1373c69b16f37245b0c6ddf6926346384 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 1 Jul 2022 16:09:07 -0400 Subject: [PATCH 03/12] Build Feed Suggestions --- app/soapbox/actions/suggestions.ts | 73 +++++++++++++++- app/soapbox/actions/timelines.ts | 29 ++++--- app/soapbox/components/status_list.tsx | 13 ++- .../feed-suggestions/feed-suggestions.tsx | 85 +++++++++++++++++++ .../features/follow-recommendations/index.tsx | 82 ++++++++++++++++++ .../components/account.tsx | 46 ---------- .../follow_recommendations_container.tsx | 30 ------- .../follow_recommendations_list.tsx | 44 ---------- .../features/follow_recommendations/index.tsx | 22 ----- app/soapbox/features/ui/index.tsx | 5 +- .../features/ui/util/async-components.ts | 2 +- app/soapbox/reducers/suggestions.ts | 13 +++ app/soapbox/reducers/timelines.ts | 19 ++++- app/soapbox/utils/features.ts | 5 ++ 14 files changed, 304 insertions(+), 164 deletions(-) create mode 100644 app/soapbox/features/feed-suggestions/feed-suggestions.tsx create mode 100644 app/soapbox/features/follow-recommendations/index.tsx delete mode 100644 app/soapbox/features/follow_recommendations/components/account.tsx delete mode 100644 app/soapbox/features/follow_recommendations/components/follow_recommendations_container.tsx delete mode 100644 app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx delete mode 100644 app/soapbox/features/follow_recommendations/index.tsx diff --git a/app/soapbox/actions/suggestions.ts b/app/soapbox/actions/suggestions.ts index d82f81f40..86743f5aa 100644 --- a/app/soapbox/actions/suggestions.ts +++ b/app/soapbox/actions/suggestions.ts @@ -1,3 +1,5 @@ +import { AxiosResponse } from 'axios'; + import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures } from 'soapbox/utils/features'; @@ -5,6 +7,7 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; +import { insertSuggestionsIntoTimeline } from './timelines'; import type { AppDispatch, RootState } from 'soapbox/store'; import type { APIEntity } from 'soapbox/types/entities'; @@ -19,6 +22,10 @@ const SUGGESTIONS_V2_FETCH_REQUEST = 'SUGGESTIONS_V2_FETCH_REQUEST'; const SUGGESTIONS_V2_FETCH_SUCCESS = 'SUGGESTIONS_V2_FETCH_SUCCESS'; const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL'; +const SUGGESTIONS_TRUTH_FETCH_REQUEST = 'SUGGESTIONS_TRUTH_FETCH_REQUEST'; +const SUGGESTIONS_TRUTH_FETCH_SUCCESS = 'SUGGESTIONS_TRUTH_FETCH_SUCCESS'; +const SUGGESTIONS_TRUTH_FETCH_FAIL = 'SUGGESTIONS_TRUTH_FETCH_FAIL'; + const fetchSuggestionsV1 = (params: Record = {}) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true }); @@ -52,6 +59,48 @@ const fetchSuggestionsV2 = (params: Record = {}) => }); }; +export type SuggestedProfile = { + account_avatar: string + account_id: string + acct: string + display_name: string + note: string + verified: boolean +} + +const mapSuggestedProfileToAccount = (suggestedProfile: SuggestedProfile) => ({ + id: suggestedProfile.account_id, + avatar: suggestedProfile.account_avatar, + avatar_static: suggestedProfile.account_avatar, + acct: suggestedProfile.acct, + display_name: suggestedProfile.display_name, + note: suggestedProfile.note, + verified: suggestedProfile.verified, +}); + +const fetchTruthSuggestions = (params: Record = {}) => + (dispatch: AppDispatch, getState: () => RootState) => { + const next = getState().suggestions.next; + + dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true }); + + return api(getState) + .get(next ? next : '/api/v1/truth/carousels/suggestions', next ? {} : { params }) + .then((response: AxiosResponse) => { + const suggestedProfiles = response.data; + const next = getLinks(response).refs.find(link => link.rel === 'next')?.uri; + + const accounts = suggestedProfiles.map(mapSuggestedProfileToAccount); + dispatch(importFetchedAccounts(accounts)); + dispatch({ type: SUGGESTIONS_TRUTH_FETCH_SUCCESS, suggestions: suggestedProfiles, next, skipLoading: true }); + return suggestedProfiles; + }) + .catch(error => { + dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true }); + throw error; + }); + }; + const fetchSuggestions = (params: Record = { limit: 50 }) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -59,17 +108,24 @@ const fetchSuggestions = (params: Record = { limit: 50 }) => const instance = state.instance; const features = getFeatures(instance); - if (!me) return; + if (!me) return null; - if (features.suggestionsV2) { - dispatch(fetchSuggestionsV2(params)) + if (features.truthSuggestions) { + return dispatch(fetchTruthSuggestions(params)) + .then((suggestions: APIEntity[]) => { + const accountIds = suggestions.map((account) => account.account_id); + dispatch(fetchRelationships(accountIds)); + }) + .catch(() => { }); + } else if (features.suggestionsV2) { + return dispatch(fetchSuggestionsV2(params)) .then((suggestions: APIEntity[]) => { const accountIds = suggestions.map(({ account }) => account.id); dispatch(fetchRelationships(accountIds)); }) .catch(() => { }); } else if (features.suggestions) { - dispatch(fetchSuggestionsV1(params)) + return dispatch(fetchSuggestionsV1(params)) .then((accounts: APIEntity[]) => { const accountIds = accounts.map(({ id }) => id); dispatch(fetchRelationships(accountIds)); @@ -77,9 +133,14 @@ const fetchSuggestions = (params: Record = { limit: 50 }) => .catch(() => { }); } else { // Do nothing + return null; } }; +const fetchSuggestionsForTimeline = () => (dispatch: AppDispatch, _getState: () => RootState) => { + dispatch(fetchSuggestions({ limit: 20 }))?.then(() => dispatch(insertSuggestionsIntoTimeline())); +}; + const dismissSuggestion = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; @@ -100,8 +161,12 @@ export { SUGGESTIONS_V2_FETCH_REQUEST, SUGGESTIONS_V2_FETCH_SUCCESS, SUGGESTIONS_V2_FETCH_FAIL, + SUGGESTIONS_TRUTH_FETCH_REQUEST, + SUGGESTIONS_TRUTH_FETCH_SUCCESS, + SUGGESTIONS_TRUTH_FETCH_FAIL, fetchSuggestionsV1, fetchSuggestionsV2, fetchSuggestions, + fetchSuggestionsForTimeline, dismissSuggestion, }; diff --git a/app/soapbox/actions/timelines.ts b/app/soapbox/actions/timelines.ts index a962b068c..4528d4bd7 100644 --- a/app/soapbox/actions/timelines.ts +++ b/app/soapbox/actions/timelines.ts @@ -12,21 +12,22 @@ import type { AxiosError } from 'axios'; import type { AppDispatch, RootState } from 'soapbox/store'; import type { APIEntity, Status } from 'soapbox/types/entities'; -const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; -const TIMELINE_DELETE = 'TIMELINE_DELETE'; -const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; +const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; +const TIMELINE_DELETE = 'TIMELINE_DELETE'; +const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE'; const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE'; const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; -const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; +const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; -const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; +const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; const TIMELINE_REPLACE = 'TIMELINE_REPLACE'; +const TIMELINE_INSERT = 'TIMELINE_INSERT'; const MAX_QUEUED_ITEMS = 40; @@ -110,9 +111,9 @@ const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string) const deleteFromTimelines = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const accountId = getState().statuses.get(id)?.account; + const accountId = getState().statuses.get(id)?.account; const references = getState().statuses.filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]); - const reblogOf = getState().statuses.getIn([id, 'reblog'], null); + const reblogOf = getState().statuses.getIn([id, 'reblog'], null); dispatch({ type: TIMELINE_DELETE, @@ -127,7 +128,7 @@ const clearTimeline = (timeline: string) => (dispatch: AppDispatch) => dispatch({ type: TIMELINE_CLEAR, timeline }); -const noOp = () => {}; +const noOp = () => { }; const noOpAsync = () => () => new Promise(f => f(undefined)); const parseTags = (tags: Record = {}, mode: 'any' | 'all' | 'none') => { @@ -214,9 +215,9 @@ const expandGroupTimeline = (id: string, { maxId }: Record = {}, do const expandHashtagTimeline = (hashtag: string, { maxId, tags }: Record = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, - any: parseTags(tags, 'any'), - all: parseTags(tags, 'all'), - none: parseTags(tags, 'none'), + any: parseTags(tags, 'any'), + all: parseTags(tags, 'all'), + none: parseTags(tags, 'none'), }, done); }; @@ -259,6 +260,10 @@ const scrollTopTimeline = (timeline: string, top: boolean) => ({ top, }); +const insertSuggestionsIntoTimeline = () => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: TIMELINE_INSERT, timeline: 'home' }); +}; + export { TIMELINE_UPDATE, TIMELINE_DELETE, @@ -272,6 +277,7 @@ export { TIMELINE_CONNECT, TIMELINE_DISCONNECT, TIMELINE_REPLACE, + TIMELINE_INSERT, MAX_QUEUED_ITEMS, processTimelineUpdate, updateTimeline, @@ -298,4 +304,5 @@ export { connectTimeline, disconnectTimeline, scrollTopTimeline, + insertSuggestionsIntoTimeline, }; diff --git a/app/soapbox/components/status_list.tsx b/app/soapbox/components/status_list.tsx index 7a86778eb..62f67910e 100644 --- a/app/soapbox/components/status_list.tsx +++ b/app/soapbox/components/status_list.tsx @@ -6,8 +6,13 @@ import { FormattedMessage } from 'react-intl'; import LoadGap from 'soapbox/components/load_gap'; import ScrollableList from 'soapbox/components/scrollable_list'; import StatusContainer from 'soapbox/containers/status_container'; +import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; import PendingStatus from 'soapbox/features/ui/components/pending_status'; +import { useAppSelector } from 'soapbox/hooks'; + +import { Button, Card, CardBody, CardTitle, HStack, Stack, Text } from './ui'; +import VerificationBadge from './verification_badge'; import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; import type { VirtuosoHandle } from 'react-virtuoso'; @@ -77,7 +82,7 @@ const StatusList: React.FC = ({ const handleLoadOlder = useCallback(debounce(() => { const maxId = lastStatusId || statusIds.last(); if (onLoadMore && maxId) { - onLoadMore(maxId); + onLoadMore(maxId.replace('末suggestions-', '')); } }, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds.last()]); @@ -149,11 +154,17 @@ const StatusList: React.FC = ({ )); }; + const renderFeedSuggestions = (): React.ReactNode => { + return ; + }; + const renderStatuses = (): React.ReactNode[] => { if (isLoading || statusIds.size > 0) { return statusIds.toArray().map((statusId, index) => { if (statusId === null) { return renderLoadGap(index); + } else if (statusId.startsWith('末suggestions-')) { + return renderFeedSuggestions(); } else if (statusId.startsWith('末pending-')) { return renderPendingStatus(statusId); } else { diff --git a/app/soapbox/features/feed-suggestions/feed-suggestions.tsx b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx new file mode 100644 index 000000000..a1add25ee --- /dev/null +++ b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import VerificationBadge from 'soapbox/components/verification_badge'; +import { useAccount, useAppSelector } from 'soapbox/hooks'; + +import { Card, CardBody, CardTitle, HStack, Stack, Text } from '../../components/ui'; +import ActionButton from '../ui/components/action-button'; + +import type { Account } from 'soapbox/types/entities'; + +const SuggestionItem = ({ accountId }: { accountId: string }) => { + const account = useAccount(accountId) as Account; + + return ( + + + + {account.acct} + + + + + + {account.verified && } + + + @{account.acct} + + + + +
+ +
+
+ ); +}; + +const FeedSuggestions = () => { + const suggestedProfiles = useAppSelector((state) => state.suggestions.items); + + return ( + + + + + + View all + + + + + + {suggestedProfiles.slice(0, 4).map((suggestedProfile) => ( + + ))} + + + + ); +}; + +export default FeedSuggestions; diff --git a/app/soapbox/features/follow-recommendations/index.tsx b/app/soapbox/features/follow-recommendations/index.tsx new file mode 100644 index 000000000..18f230196 --- /dev/null +++ b/app/soapbox/features/follow-recommendations/index.tsx @@ -0,0 +1,82 @@ +import debounce from 'lodash/debounce'; +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router-dom'; + +import { fetchSuggestions } from 'soapbox/actions/suggestions'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Stack, Text } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import Column from 'soapbox/features/ui/components/column'; +import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; + +const FollowRecommendations: React.FC = () => { + const dispatch = useAppDispatch(); + const features = useFeatures(); + const history = useHistory(); + + const suggestions = useAppSelector((state) => state.suggestions.items); + const hasMore = useAppSelector((state) => !!state.suggestions.next); + const isLoading = useAppSelector((state) => state.suggestions.isLoading); + + const handleLoadMore = debounce(() => { + if (isLoading) { + return null; + } + + return dispatch(fetchSuggestions({ limit: 20 })); + }, 300); + + const onDone = () => { + history.push('/'); + }; + + useEffect(() => { + dispatch(fetchSuggestions({ limit: 20 })); + }, []); + + if (suggestions.size === 0 && !isLoading) { + return ( + + + + + + ); + } + + return ( + + + + {features.truthSuggestions ? ( + suggestions.map((suggestedProfile) => ( + + )) + ) : ( + suggestions.map((suggestion) => ( + + )) + )} + + + + ); +}; + +export default FollowRecommendations; diff --git a/app/soapbox/features/follow_recommendations/components/account.tsx b/app/soapbox/features/follow_recommendations/components/account.tsx deleted file mode 100644 index 67bb18f50..000000000 --- a/app/soapbox/features/follow_recommendations/components/account.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; - -import Avatar from 'soapbox/components/avatar'; -import DisplayName from 'soapbox/components/display-name'; -import Permalink from 'soapbox/components/permalink'; -import ActionButton from 'soapbox/features/ui/components/action-button'; -import { useAppSelector } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; - -const getAccount = makeGetAccount(); - -const getFirstSentence = (str: string) => { - const arr = str.split(/(([.?!]+\s)|[.。?!\n•])/); - - return arr[0]; -}; - -interface IAccount { - id: string, -} - -const Account: React.FC = ({ id }) => { - const account = useAppSelector((state) => getAccount(state, id)); - - if (!account) return null; - - return ( -
-
- -
- - - -
{getFirstSentence(account.get('note_plain'))}
-
- -
- -
-
-
- ); -}; - -export default Account; diff --git a/app/soapbox/features/follow_recommendations/components/follow_recommendations_container.tsx b/app/soapbox/features/follow_recommendations/components/follow_recommendations_container.tsx deleted file mode 100644 index c3f198f94..000000000 --- a/app/soapbox/features/follow_recommendations/components/follow_recommendations_container.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -import { Button } from 'soapbox/components/ui'; - -import FollowRecommendationsList from './follow_recommendations_list'; - -interface IFollowRecommendationsContainer { - onDone: () => void, -} - -const FollowRecommendationsContainer: React.FC = ({ onDone }) => ( -
-
-

-

-

-
- - - -
- -
-
-); - -export default FollowRecommendationsContainer; diff --git a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx deleted file mode 100644 index e9e295d58..000000000 --- a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useEffect } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { useDispatch } from 'react-redux'; - -import { fetchSuggestions } from 'soapbox/actions/suggestions'; -import { Spinner } from 'soapbox/components/ui'; -import { useAppSelector } from 'soapbox/hooks'; - -import Account from './account'; - -const FollowRecommendationsList: React.FC = () => { - const dispatch = useDispatch(); - - const suggestions = useAppSelector((state) => state.suggestions.items); - const isLoading = useAppSelector((state) => state.suggestions.isLoading); - - useEffect(() => { - if (suggestions.size === 0) { - dispatch(fetchSuggestions()); - } - }, []); - - if (isLoading) { - return ( -
- -
- ); - } - - return ( -
- {suggestions.size > 0 ? suggestions.map((suggestion) => ( - - )) : ( -
- -
- )} -
- ); -}; - -export default FollowRecommendationsList; diff --git a/app/soapbox/features/follow_recommendations/index.tsx b/app/soapbox/features/follow_recommendations/index.tsx deleted file mode 100644 index 444504532..000000000 --- a/app/soapbox/features/follow_recommendations/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { useHistory } from 'react-router-dom'; - -import Column from 'soapbox/features/ui/components/column'; - -import FollowRecommendationsContainer from './components/follow_recommendations_container'; - -const FollowRecommendations: React.FC = () => { - const history = useHistory(); - - const onDone = () => { - history.push('/'); - }; - - return ( - - - - ); -}; - -export default FollowRecommendations; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 8cd5218d6..2251c5ab0 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -19,6 +19,7 @@ import { expandNotifications } from 'soapbox/actions/notifications'; import { register as registerPushNotifications } from 'soapbox/actions/push_notifications'; import { fetchScheduledStatuses } from 'soapbox/actions/scheduled_statuses'; import { connectUserStream } from 'soapbox/actions/streaming'; +import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions'; import { expandHomeTimeline } from 'soapbox/actions/timelines'; import Icon from 'soapbox/components/icon'; import SidebarNavigation from 'soapbox/components/sidebar-navigation'; @@ -441,7 +442,9 @@ const UI: React.FC = ({ children }) => { const loadAccountData = () => { if (!account) return; - dispatch(expandHomeTimeline()); + dispatch(expandHomeTimeline({}, () => { + dispatch(fetchSuggestionsForTimeline()); + })); dispatch(expandNotifications()) // @ts-ignore diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index a6379b130..30420cf27 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -455,7 +455,7 @@ export function WhoToFollowPanel() { } export function FollowRecommendations() { - return import(/* webpackChunkName: "features/follow_recommendations" */'../../follow_recommendations'); + return import(/* webpackChunkName: "features/follow-recommendations" */'../../follow-recommendations'); } export function Directory() { diff --git a/app/soapbox/reducers/suggestions.ts b/app/soapbox/reducers/suggestions.ts index fc095113f..8335ff9ec 100644 --- a/app/soapbox/reducers/suggestions.ts +++ b/app/soapbox/reducers/suggestions.ts @@ -10,8 +10,11 @@ import { SUGGESTIONS_V2_FETCH_REQUEST, SUGGESTIONS_V2_FETCH_SUCCESS, SUGGESTIONS_V2_FETCH_FAIL, + SUGGESTIONS_TRUTH_FETCH_SUCCESS, } from 'soapbox/actions/suggestions'; +import { SuggestedProfile } from '../actions/suggestions'; + import type { AnyAction } from 'redux'; import type { APIEntity } from 'soapbox/types/entities'; @@ -53,6 +56,14 @@ const importSuggestions = (state: State, suggestions: APIEntities, next: string }); }; +const importTruthSuggestions = (state: State, suggestions: SuggestedProfile[], next: string | null) => { + return state.withMutations(state => { + state.update('items', items => items.concat(suggestions.map(x => ({ ...x, account: x.account_id })).map(suggestion => SuggestionRecord(suggestion)))); + state.set('isLoading', false); + state.set('next', next); + }); +}; + const dismissAccount = (state: State, accountId: string) => { return state.update('items', items => items.filterNot(item => item.account === accountId)); }; @@ -70,6 +81,8 @@ export default function suggestionsReducer(state: State = ReducerRecord(), actio return importAccounts(state, action.accounts); case SUGGESTIONS_V2_FETCH_SUCCESS: return importSuggestions(state, action.suggestions, action.next); + case SUGGESTIONS_TRUTH_FETCH_SUCCESS: + return importTruthSuggestions(state, action.suggestions, action.next); case SUGGESTIONS_FETCH_FAIL: case SUGGESTIONS_V2_FETCH_FAIL: return state.set('isLoading', false); diff --git a/app/soapbox/reducers/timelines.ts b/app/soapbox/reducers/timelines.ts index a1f33417f..05961ba71 100644 --- a/app/soapbox/reducers/timelines.ts +++ b/app/soapbox/reducers/timelines.ts @@ -5,6 +5,7 @@ import { Record as ImmutableRecord, fromJS, } from 'immutable'; +import sample from 'lodash/sample'; import { ACCOUNT_BLOCK_SUCCESS, @@ -30,6 +31,7 @@ import { MAX_QUEUED_ITEMS, TIMELINE_SCROLL_TOP, TIMELINE_REPLACE, + TIMELINE_INSERT, } from '../actions/timelines'; import type { AnyAction } from 'redux'; @@ -37,7 +39,7 @@ import type { StatusVisibility } from 'soapbox/normalizers/status'; import type { APIEntity, Status } from 'soapbox/types/entities'; const TRUNCATE_LIMIT = 40; -const TRUNCATE_SIZE = 20; +const TRUNCATE_SIZE = 20; const TimelineRecord = ImmutableRecord({ unread: 0, @@ -115,7 +117,7 @@ const expandNormalizedTimeline = (state: State, timelineId: string, statuses: Im }; const updateTimeline = (state: State, timelineId: string, statusId: string) => { - const top = state.get(timelineId)?.top; + const top = state.get(timelineId)?.top; const oldIds = state.get(timelineId)?.items || ImmutableOrderedSet(); const unread = state.get(timelineId)?.unread || 0; @@ -135,8 +137,8 @@ const updateTimeline = (state: State, timelineId: string, statusId: string) => { }; const updateTimelineQueue = (state: State, timelineId: string, statusId: string) => { - const queuedIds = state.get(timelineId)?.queuedItems || ImmutableOrderedSet(); - const listedIds = state.get(timelineId)?.items || ImmutableOrderedSet(); + const queuedIds = state.get(timelineId)?.queuedItems || ImmutableOrderedSet(); + const listedIds = state.get(timelineId)?.items || ImmutableOrderedSet(); const queuedCount = state.get(timelineId)?.totalQueuedItemsCount || 0; if (queuedIds.includes(statusId)) return state; @@ -353,6 +355,15 @@ export default function timelines(state: State = initialState, action: AnyAction timeline.set('items', ImmutableOrderedSet([])); })) .update('home', TimelineRecord(), timeline => timeline.set('feedAccountId', action.accountId)); + case TIMELINE_INSERT: + return state.update(action.timeline, TimelineRecord(), timeline => timeline.withMutations(timeline => { + timeline.update('items', oldIds => { + const oldIdsArray = oldIds.toArray(); + const positionInTimeline = sample([5, 6, 7, 8, 9]) as number; + oldIdsArray.splice(positionInTimeline, 0, `末suggestions-${oldIds.last()}`); + return ImmutableOrderedSet(oldIdsArray); + }); + })); default: return state; } diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 206eaf067..0afe0c254 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -577,6 +577,11 @@ const getInstanceFeatures = (instance: Instance) => { v.software === TRUTHSOCIAL, ]), + /** + * Supports Truth suggestions. + */ + truthSuggestions: v.software === TRUTHSOCIAL, + /** * Whether the backend allows adding users you don't follow to lists. * @see POST /api/v1/lists/:id/accounts From b9d05f546c3b19d976f0c7c7eeaf89b49cdc42a8 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 1 Jul 2022 16:16:34 -0400 Subject: [PATCH 04/12] Lint --- app/soapbox/components/status_list.tsx | 4 ---- app/soapbox/features/follow-recommendations/index.tsx | 4 ---- 2 files changed, 8 deletions(-) diff --git a/app/soapbox/components/status_list.tsx b/app/soapbox/components/status_list.tsx index 62f67910e..35edc9283 100644 --- a/app/soapbox/components/status_list.tsx +++ b/app/soapbox/components/status_list.tsx @@ -9,10 +9,6 @@ import StatusContainer from 'soapbox/containers/status_container'; import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; import PendingStatus from 'soapbox/features/ui/components/pending_status'; -import { useAppSelector } from 'soapbox/hooks'; - -import { Button, Card, CardBody, CardTitle, HStack, Stack, Text } from './ui'; -import VerificationBadge from './verification_badge'; import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; import type { VirtuosoHandle } from 'react-virtuoso'; diff --git a/app/soapbox/features/follow-recommendations/index.tsx b/app/soapbox/features/follow-recommendations/index.tsx index 18f230196..7b68192de 100644 --- a/app/soapbox/features/follow-recommendations/index.tsx +++ b/app/soapbox/features/follow-recommendations/index.tsx @@ -27,10 +27,6 @@ const FollowRecommendations: React.FC = () => { return dispatch(fetchSuggestions({ limit: 20 })); }, 300); - const onDone = () => { - history.push('/'); - }; - useEffect(() => { dispatch(fetchSuggestions({ limit: 20 })); }, []); From 41d5769aa0070078640c64744349ce2a5dacd6ec Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 1 Jul 2022 16:24:04 -0400 Subject: [PATCH 05/12] Lint --- app/soapbox/features/follow-recommendations/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/soapbox/features/follow-recommendations/index.tsx b/app/soapbox/features/follow-recommendations/index.tsx index 7b68192de..7ad9c7ad3 100644 --- a/app/soapbox/features/follow-recommendations/index.tsx +++ b/app/soapbox/features/follow-recommendations/index.tsx @@ -1,7 +1,6 @@ import debounce from 'lodash/debounce'; import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { fetchSuggestions } from 'soapbox/actions/suggestions'; import ScrollableList from 'soapbox/components/scrollable_list'; @@ -13,7 +12,6 @@ import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; const FollowRecommendations: React.FC = () => { const dispatch = useAppDispatch(); const features = useFeatures(); - const history = useHistory(); const suggestions = useAppSelector((state) => state.suggestions.items); const hasMore = useAppSelector((state) => !!state.suggestions.next); From b46ccc8b3e4f92b0f6431ccd4f2918b229534d69 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 6 Jul 2022 08:10:10 -0400 Subject: [PATCH 06/12] Add tests for suggestions action --- .../actions/__tests__/suggestions.test.ts | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 app/soapbox/actions/__tests__/suggestions.test.ts diff --git a/app/soapbox/actions/__tests__/suggestions.test.ts b/app/soapbox/actions/__tests__/suggestions.test.ts new file mode 100644 index 000000000..76e53d576 --- /dev/null +++ b/app/soapbox/actions/__tests__/suggestions.test.ts @@ -0,0 +1,108 @@ +import { Map as ImmutableMap } from 'immutable'; + +import { __stub } from 'soapbox/api'; +import { mockStore } from 'soapbox/jest/test-helpers'; +import rootReducer from 'soapbox/reducers'; + +import { + fetchSuggestions, +} from '../suggestions'; + +let store; +let state; + +describe('fetchSuggestions()', () => { + describe('with Truth Social software', () => { + beforeEach(() => { + state = rootReducer(undefined, {}) + .set('instance', { + version: '3.4.1 (compatible; TruthSocial 1.0.0)', + pleroma: ImmutableMap({ + metadata: ImmutableMap({ + features: [], + }), + }), + }) + .set('me', '123'); + store = mockStore(state); + }); + + describe('with a successful API request', () => { + const response = [ + { + account_id: '1', + acct: 'jl', + account_avatar: 'https://example.com/some.jpg', + display_name: 'justin', + note: '

note

', + verified: true, + }, + ]; + + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions').reply(200, response, { + link: '; rel=\'prev\'', + }); + }); + }); + + it('dispatches the correct actions', async() => { + const expectedActions = [ + { type: 'SUGGESTIONS_V2_FETCH_REQUEST', skipLoading: true }, + { + type: 'ACCOUNTS_IMPORT', accounts: [{ + acct: response[0].acct, + avatar: response[0].account_avatar, + avatar_static: response[0].account_avatar, + id: response[0].account_id, + note: response[0].note, + verified: response[0].verified, + display_name: response[0].display_name, + }], + }, + { + type: 'SUGGESTIONS_TRUTH_FETCH_SUCCESS', + suggestions: response, + next: undefined, + skipLoading: true, + }, + { + type: 'RELATIONSHIPS_FETCH_REQUEST', + skipLoading: true, + ids: [response[0].account_id], + }, + ]; + await store.dispatch(fetchSuggestions()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an unsuccessful API request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions').networkError(); + }); + }); + + it('should dispatch the correct actions', async() => { + const expectedActions = [ + { type: 'SUGGESTIONS_V2_FETCH_REQUEST', skipLoading: true }, + { + type: 'SUGGESTIONS_V2_FETCH_FAIL', + error: new Error('Network Error'), + skipLoading: true, + skipAlert: true, + }, + ]; + + await store.dispatch(fetchSuggestions()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + }); +}); From 7b9a9c8e3481d0d8632d0cc79ff09f1f92d491af Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 6 Jul 2022 08:41:15 -0400 Subject: [PATCH 07/12] Fix overflow of text in feed filtering --- app/soapbox/features/feed-filtering/feed-carousel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/feed-filtering/feed-carousel.tsx b/app/soapbox/features/feed-filtering/feed-carousel.tsx index 6a50f39df..8fcf643c1 100644 --- a/app/soapbox/features/feed-filtering/feed-carousel.tsx +++ b/app/soapbox/features/feed-filtering/feed-carousel.tsx @@ -41,7 +41,7 @@ const CarouselItem = ({ avatar }: { avatar: any }) => { />
- {avatar.acct} + {avatar.acct} ); From 2f465fbc141584b687afabed0f92a2cd0923f0f6 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 6 Jul 2022 08:53:58 -0400 Subject: [PATCH 08/12] Use same value as 'showProfileHoverCard' --- app/soapbox/components/quoted-status.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/components/quoted-status.tsx b/app/soapbox/components/quoted-status.tsx index 82fe8860a..431263bd1 100644 --- a/app/soapbox/components/quoted-status.tsx +++ b/app/soapbox/components/quoted-status.tsx @@ -137,7 +137,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => timestamp={status.created_at} withRelationship={false} showProfileHoverCard={!compose} - withLinkToProfile={false} + withLinkToProfile={!compose} /> {renderReplyMentions()} From c664844e3c04d167d6fe56e57467772bfddc4a96 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 6 Jul 2022 09:04:21 -0400 Subject: [PATCH 09/12] Persist suggestions when filtering feed --- app/soapbox/actions/timelines.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/actions/timelines.ts b/app/soapbox/actions/timelines.ts index 4528d4bd7..74622d180 100644 --- a/app/soapbox/actions/timelines.ts +++ b/app/soapbox/actions/timelines.ts @@ -142,7 +142,7 @@ const replaceHomeTimeline = ( { maxId }: Record = {}, ) => (dispatch: AppDispatch, _getState: () => RootState) => { dispatch({ type: TIMELINE_REPLACE, accountId }); - dispatch(expandHomeTimeline({ accountId, maxId })); + dispatch(expandHomeTimeline({ accountId, maxId }, () => dispatch(insertSuggestionsIntoTimeline()))); }; const expandTimeline = (timelineId: string, path: string, params: Record = {}, done = noOp) => From b70627168766b7065b110fae5596671bc77635b8 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 6 Jul 2022 09:04:26 -0400 Subject: [PATCH 10/12] Intl --- .../features/feed-suggestions/feed-suggestions.tsx | 11 +++++++++-- app/soapbox/features/follow-recommendations/index.tsx | 11 ++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/soapbox/features/feed-suggestions/feed-suggestions.tsx b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx index a1add25ee..a5740abf9 100644 --- a/app/soapbox/features/feed-suggestions/feed-suggestions.tsx +++ b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import VerificationBadge from 'soapbox/components/verification_badge'; @@ -9,6 +10,11 @@ import ActionButton from '../ui/components/action-button'; import type { Account } from 'soapbox/types/entities'; +const messages = defineMessages({ + heading: { id: 'feedSuggestions.heading', defaultMessage: 'Suggested profiles' }, + viewAll: { id: 'feedSuggestions.viewAll', defaultMessage: 'View all' }, +}); + const SuggestionItem = ({ accountId }: { accountId: string }) => { const account = useAccount(accountId) as Account; @@ -52,18 +58,19 @@ const SuggestionItem = ({ accountId }: { accountId: string }) => { }; const FeedSuggestions = () => { + const intl = useIntl(); const suggestedProfiles = useAppSelector((state) => state.suggestions.items); return ( - + - View all + {intl.formatMessage(messages.viewAll)} diff --git a/app/soapbox/features/follow-recommendations/index.tsx b/app/soapbox/features/follow-recommendations/index.tsx index 7ad9c7ad3..235142aa4 100644 --- a/app/soapbox/features/follow-recommendations/index.tsx +++ b/app/soapbox/features/follow-recommendations/index.tsx @@ -1,6 +1,6 @@ import debounce from 'lodash/debounce'; import React, { useEffect } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { fetchSuggestions } from 'soapbox/actions/suggestions'; import ScrollableList from 'soapbox/components/scrollable_list'; @@ -9,8 +9,13 @@ import AccountContainer from 'soapbox/containers/account_container'; import Column from 'soapbox/features/ui/components/column'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; +const messages = defineMessages({ + heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested profiles' }, +}); + const FollowRecommendations: React.FC = () => { const dispatch = useAppDispatch(); + const intl = useIntl(); const features = useFeatures(); const suggestions = useAppSelector((state) => state.suggestions.items); @@ -31,7 +36,7 @@ const FollowRecommendations: React.FC = () => { if (suggestions.size === 0 && !isLoading) { return ( - + @@ -40,7 +45,7 @@ const FollowRecommendations: React.FC = () => { } return ( - + Date: Wed, 6 Jul 2022 14:36:28 -0400 Subject: [PATCH 11/12] Improve alignment of actions --- app/soapbox/features/follow-recommendations/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/features/follow-recommendations/index.tsx b/app/soapbox/features/follow-recommendations/index.tsx index 235142aa4..7fda03c7a 100644 --- a/app/soapbox/features/follow-recommendations/index.tsx +++ b/app/soapbox/features/follow-recommendations/index.tsx @@ -61,6 +61,7 @@ const FollowRecommendations: React.FC = () => { id={suggestedProfile.account} withAccountNote showProfileHoverCard={false} + actionAlignment='top' /> )) ) : ( From cb26a515a2cc32de58cd0c1459465c9b32cad6dd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 9 Jul 2022 15:08:01 -0500 Subject: [PATCH 12/12] Fix suggestions test types --- app/soapbox/actions/__tests__/suggestions.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/soapbox/actions/__tests__/suggestions.test.ts b/app/soapbox/actions/__tests__/suggestions.test.ts index 76e53d576..3c8d0c95a 100644 --- a/app/soapbox/actions/__tests__/suggestions.test.ts +++ b/app/soapbox/actions/__tests__/suggestions.test.ts @@ -1,28 +1,28 @@ import { Map as ImmutableMap } from 'immutable'; import { __stub } from 'soapbox/api'; -import { mockStore } from 'soapbox/jest/test-helpers'; -import rootReducer from 'soapbox/reducers'; +import { mockStore, rootState } from 'soapbox/jest/test-helpers'; +import { normalizeInstance } from 'soapbox/normalizers'; import { fetchSuggestions, } from '../suggestions'; -let store; +let store: ReturnType; let state; describe('fetchSuggestions()', () => { describe('with Truth Social software', () => { beforeEach(() => { - state = rootReducer(undefined, {}) - .set('instance', { + state = rootState + .set('instance', normalizeInstance({ version: '3.4.1 (compatible; TruthSocial 1.0.0)', pleroma: ImmutableMap({ metadata: ImmutableMap({ features: [], }), }), - }) + })) .set('me', '123'); store = mockStore(state); });