From 170743d448b1cd6f44c78788607222aead89cd91 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Fri, 2 Jun 2023 10:41:07 -0400 Subject: [PATCH 1/7] Fetch group relationship from notifications --- app/soapbox/actions/notifications.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/notifications.ts b/app/soapbox/actions/notifications.ts index 7b91b64d8..a7c2f11f6 100644 --- a/app/soapbox/actions/notifications.ts +++ b/app/soapbox/actions/notifications.ts @@ -12,6 +12,7 @@ import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification'; import { joinPublicPath } from 'soapbox/utils/static'; import { fetchRelationships } from './accounts'; +import { fetchGroupRelationships } from './groups'; import { importFetchedAccount, importFetchedAccounts, @@ -23,7 +24,7 @@ import { getSettings, saveSettings } from './settings'; import type { AxiosError } from 'axios'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity } from 'soapbox/types/entities'; +import type { APIEntity, Status } from 'soapbox/types/entities'; const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -237,6 +238,9 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an dispatch(importFetchedAccounts(Object.values(entries.accounts))); dispatch(importFetchedStatuses(Object.values(entries.statuses))); + const statusesFromGroups = (Object.values(entries.statuses) as Status[]).filter((status) => !!status.group); + dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id))); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); fetchRelatedRelationships(dispatch, response.data); done(); From c82ece5a1920e39e4a9e0c9a7a7f875a330cba5c Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Tue, 6 Jun 2023 09:19:11 -0400 Subject: [PATCH 2/7] Fetch group relationship from timeline --- app/soapbox/actions/timelines.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/soapbox/actions/timelines.ts b/app/soapbox/actions/timelines.ts index 902b99f70..ee94292dd 100644 --- a/app/soapbox/actions/timelines.ts +++ b/app/soapbox/actions/timelines.ts @@ -6,6 +6,7 @@ import { shouldFilter } from 'soapbox/utils/timelines'; import api, { getNextLink, getPrevLink } from '../api'; +import { fetchGroupRelationships } from './groups'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import type { AxiosError } from 'axios'; @@ -177,6 +178,10 @@ const expandTimeline = (timelineId: string, path: string, params: Record { dispatch(importFetchedStatuses(response.data)); + + const statusesFromGroups = (response.data as Status[]).filter((status) => !!status.group); + dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id))); + dispatch(expandTimelineSuccess( timelineId, response.data, From e3f92eadace3048c4496c5521a5978b41da40561 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 14 Jun 2023 08:05:25 -0400 Subject: [PATCH 3/7] Add Groups to Thumb Navigation --- app/soapbox/components/thumb-navigation.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/thumb-navigation.tsx b/app/soapbox/components/thumb-navigation.tsx index 6abaa084e..013ecece7 100644 --- a/app/soapbox/components/thumb-navigation.tsx +++ b/app/soapbox/components/thumb-navigation.tsx @@ -3,15 +3,17 @@ import { FormattedMessage } from 'react-intl'; import ThumbNavigationLink from 'soapbox/components/thumb-navigation-link'; import { useStatContext } from 'soapbox/contexts/stat-context'; -import { useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks'; +import { useAppSelector, useFeatures, useGroupsPath, useOwnAccount } from 'soapbox/hooks'; const ThumbNavigation: React.FC = (): JSX.Element => { const account = useOwnAccount(); + const features = useFeatures(); + const groupsPath = useGroupsPath(); + const { unreadChatsCount } = useStatContext(); const notificationCount = useAppSelector((state) => state.notifications.unread); const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); - const features = useFeatures(); /** Conditionally render the supported messages link */ const renderMessagesLink = (): React.ReactNode => { @@ -51,6 +53,15 @@ const ThumbNavigation: React.FC = (): JSX.Element => { exact /> + {features.groups && ( + } + to={groupsPath} + exact + /> + )} + } From 1d9130f7acee0ba697088f09d5e05e7015386022 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 14 Jun 2023 08:11:39 -0400 Subject: [PATCH 4/7] Add Suggested Groups panel to Search page --- app/soapbox/features/ui/index.tsx | 3 +- app/soapbox/pages/search-page.tsx | 67 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/pages/search-page.tsx diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index dcdd7fecb..6cd41dde5 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -38,6 +38,7 @@ import HomePage from 'soapbox/pages/home-page'; import ManageGroupsPage from 'soapbox/pages/manage-groups-page'; import ProfilePage from 'soapbox/pages/profile-page'; import RemoteInstancePage from 'soapbox/pages/remote-instance-page'; +import SearchPage from 'soapbox/pages/search-page'; import StatusPage from 'soapbox/pages/status-page'; import { usePendingPolicy } from 'soapbox/queries/policies'; import { getAccessToken, getVapidKey } from 'soapbox/utils/auth'; @@ -275,7 +276,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => - + {features.suggestions && } {features.profileDirectory && } {features.events && } diff --git a/app/soapbox/pages/search-page.tsx b/app/soapbox/pages/search-page.tsx new file mode 100644 index 000000000..c032fbd26 --- /dev/null +++ b/app/soapbox/pages/search-page.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +import LinkFooter from 'soapbox/features/ui/components/link-footer'; +import BundleContainer from 'soapbox/features/ui/containers/bundle-container'; +import { + WhoToFollowPanel, + TrendsPanel, + SignUpPanel, + CtaBanner, + SuggestedGroupsPanel, +} from 'soapbox/features/ui/util/async-components'; +import { useAppSelector, useFeatures } from 'soapbox/hooks'; + +import { Layout } from '../components/ui'; + +interface ISearchPage { + children: React.ReactNode +} + +const SearchPage: React.FC = ({ children }) => { + const me = useAppSelector(state => state.me); + const features = useFeatures(); + + return ( + <> + + {children} + + {!me && ( + + {Component => } + + )} + + + + {!me && ( + + {Component => } + + )} + + {features.trends && ( + + {Component => } + + )} + + {me && features.suggestions && ( + + {Component => } + + )} + + {features.groups && ( + + {Component => } + + )} + + + + + ); +}; + +export default SearchPage; From a985348bf1f38cda18ad9f2a48418632444bf471 Mon Sep 17 00:00:00 2001 From: oakes Date: Thu, 15 Jun 2023 09:05:55 -0400 Subject: [PATCH 5/7] Optionally use Link header for search pagination --- app/soapbox/actions/search.ts | 41 +++++++++++++++++++++++----------- app/soapbox/reducers/search.ts | 11 +++++---- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/app/soapbox/actions/search.ts b/app/soapbox/actions/search.ts index 3f8d2011e..2b4c8f4e9 100644 --- a/app/soapbox/actions/search.ts +++ b/app/soapbox/actions/search.ts @@ -1,4 +1,4 @@ -import api from '../api'; +import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts, importFetchedStatuses } from './importer'; @@ -83,7 +83,9 @@ const submitSearch = (filter?: SearchFilter) => dispatch(importFetchedStatuses(response.data.statuses)); } - dispatch(fetchSearchSuccess(response.data, value, type)); + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(fetchSearchSuccess(response.data, value, type, next ? next.uri : null)); dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id))); }).catch(error => { dispatch(fetchSearchFail(error)); @@ -95,11 +97,12 @@ const fetchSearchRequest = (value: string) => ({ value, }); -const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({ +const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({ type: SEARCH_FETCH_SUCCESS, results, searchTerm, searchType, + next, }); const fetchSearchFail = (error: AxiosError) => ({ @@ -125,17 +128,26 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: ( dispatch(expandSearchRequest(type)); - const params: Record = { - q: value, - type, - offset, - }; + let url = getState().search.next as string; + let params: Record = {}; - if (accountId) params.account_id = accountId; + // if no URL was extracted from the Link header, + // fall back on querying with the offset + if (!url) { + url = '/api/v2/search'; + params = { + q: value, + type, + offset, + }; + if (accountId) params.account_id = accountId; + } - api(getState).get('/api/v2/search', { + api(getState).get(url, { params, - }).then(({ data }) => { + }).then(response => { + const data = response.data; + if (data.accounts) { dispatch(importFetchedAccounts(data.accounts)); } @@ -144,7 +156,9 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: ( dispatch(importFetchedStatuses(data.statuses)); } - dispatch(expandSearchSuccess(data, value, type)); + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(expandSearchSuccess(data, value, type, next ? next.uri : null)); dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id))); }).catch(error => { dispatch(expandSearchFail(error)); @@ -156,11 +170,12 @@ const expandSearchRequest = (searchType: SearchFilter) => ({ searchType, }); -const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({ +const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({ type: SEARCH_EXPAND_SUCCESS, results, searchTerm, searchType, + next, }); const expandSearchFail = (error: AxiosError) => ({ diff --git a/app/soapbox/reducers/search.ts b/app/soapbox/reducers/search.ts index abc633533..f4e299290 100644 --- a/app/soapbox/reducers/search.ts +++ b/app/soapbox/reducers/search.ts @@ -47,6 +47,7 @@ const ReducerRecord = ImmutableRecord({ results: ResultsRecord(), filter: 'accounts' as SearchFilter, accountId: null as string | null, + next: null as string | null, }); type State = ReturnType; @@ -57,7 +58,7 @@ const toIds = (items: APIEntities = []) => { return ImmutableOrderedSet(items.map(item => item.id)); }; -const importResults = (state: State, results: APIEntity, searchTerm: string, searchType: SearchFilter) => { +const importResults = (state: State, results: APIEntity, searchTerm: string, searchType: SearchFilter, next: string | null) => { return state.withMutations(state => { if (state.value === searchTerm && state.filter === searchType) { state.set('results', ResultsRecord({ @@ -76,15 +77,17 @@ const importResults = (state: State, results: APIEntity, searchTerm: string, sea })); state.set('submitted', true); + state.set('next', next); } }); }; -const paginateResults = (state: State, searchType: SearchFilter, results: APIEntity, searchTerm: string) => { +const paginateResults = (state: State, searchType: SearchFilter, results: APIEntity, searchTerm: string, next: string | null) => { return state.withMutations(state => { if (state.value === searchTerm) { state.setIn(['results', `${searchType}HasMore`], results[searchType].length >= 20); state.setIn(['results', `${searchType}Loaded`], true); + state.set('next', next); state.updateIn(['results', searchType], items => { const data = results[searchType]; // Hashtags are a list of maps. Others are IDs. @@ -129,13 +132,13 @@ export default function search(state = ReducerRecord(), action: AnyAction) { case SEARCH_FETCH_REQUEST: return handleSubmitted(state, action.value); case SEARCH_FETCH_SUCCESS: - return importResults(state, action.results, action.searchTerm, action.searchType); + return importResults(state, action.results, action.searchTerm, action.searchType, action.next); case SEARCH_FILTER_SET: return state.set('filter', action.value); case SEARCH_EXPAND_REQUEST: return state.setIn(['results', `${action.searchType}Loaded`], false); case SEARCH_EXPAND_SUCCESS: - return paginateResults(state, action.searchType, action.results, action.searchTerm); + return paginateResults(state, action.searchType, action.results, action.searchTerm, action.next); case SEARCH_ACCOUNT_SET: if (!action.accountId) return state.merge({ results: ResultsRecord(), From e1cacb6ee48204a8a50080c2afc37514d5ca709d Mon Sep 17 00:00:00 2001 From: oakes Date: Thu, 15 Jun 2023 14:57:58 -0400 Subject: [PATCH 6/7] Optionally use Link header for pagination in various timelines --- app/soapbox/actions/timelines.ts | 32 +++++++++---------- .../features/account-gallery/index.tsx | 3 +- .../features/account-timeline/index.tsx | 3 +- .../features/community-timeline/index.tsx | 5 +-- .../features/direct-timeline/index.tsx | 5 +-- .../features/hashtag-timeline/index.tsx | 3 +- app/soapbox/features/list-timeline/index.tsx | 3 +- .../features/public-timeline/index.tsx | 5 +-- .../features/remote-timeline/index.tsx | 5 +-- 9 files changed, 36 insertions(+), 28 deletions(-) diff --git a/app/soapbox/actions/timelines.ts b/app/soapbox/actions/timelines.ts index 902b99f70..b555a3b38 100644 --- a/app/soapbox/actions/timelines.ts +++ b/app/soapbox/actions/timelines.ts @@ -221,29 +221,29 @@ const expandHomeTimeline = ({ url, accountId, maxId }: ExpandHomeTimelineOpts = return expandTimeline('home', endpoint, params, done); }; -const expandPublicTimeline = ({ maxId, onlyMedia }: Record = {}, done = noOp) => - expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); +const expandPublicTimeline = ({ url, maxId, onlyMedia }: Record = {}, done = noOp) => + expandTimeline(`public${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/public', url ? {} : { max_id: maxId, only_media: !!onlyMedia }, done); -const expandRemoteTimeline = (instance: string, { maxId, onlyMedia }: Record = {}, done = noOp) => - expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, '/api/v1/timelines/public', { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done); +const expandRemoteTimeline = (instance: string, { url, maxId, onlyMedia }: Record = {}, done = noOp) => + expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, url || '/api/v1/timelines/public', url ? {} : { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done); -const expandCommunityTimeline = ({ maxId, onlyMedia }: Record = {}, done = noOp) => - expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); +const expandCommunityTimeline = ({ url, maxId, onlyMedia }: Record = {}, done = noOp) => + expandTimeline(`community${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/public', url ? {} : { local: true, max_id: maxId, only_media: !!onlyMedia }, done); -const expandDirectTimeline = ({ maxId }: Record = {}, done = noOp) => - expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); +const expandDirectTimeline = ({ url, maxId }: Record = {}, done = noOp) => + expandTimeline('direct', url || '/api/v1/timelines/direct', url ? {} : { max_id: maxId }, done); -const expandAccountTimeline = (accountId: string, { maxId, withReplies }: Record = {}) => - expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId, with_muted: true }); +const expandAccountTimeline = (accountId: string, { url, maxId, withReplies }: Record = {}) => + expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, url || `/api/v1/accounts/${accountId}/statuses`, url ? {} : { exclude_replies: !withReplies, max_id: maxId, with_muted: true }); const expandAccountFeaturedTimeline = (accountId: string) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, with_muted: true }); -const expandAccountMediaTimeline = (accountId: string | number, { maxId }: Record = {}) => - expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40, with_muted: true }); +const expandAccountMediaTimeline = (accountId: string | number, { url, maxId }: Record = {}) => + expandTimeline(`account:${accountId}:media`, url || `/api/v1/accounts/${accountId}/statuses`, url ? {} : { max_id: maxId, only_media: true, limit: 40, with_muted: true }); -const expandListTimeline = (id: string, { maxId }: Record = {}, done = noOp) => - expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); +const expandListTimeline = (id: string, { url, maxId }: Record = {}, done = noOp) => + expandTimeline(`list:${id}`, url || `/api/v1/timelines/list/${id}`, url ? {} : { max_id: maxId }, done); const expandGroupTimeline = (id: string, { maxId }: Record = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done); @@ -254,8 +254,8 @@ const expandGroupTimelineFromTag = (id: string, tagName: string, { maxId }: Reco const expandGroupMediaTimeline = (id: string | number, { maxId }: Record = {}) => expandTimeline(`group:${id}:media`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: true, limit: 40, with_muted: true }); -const expandHashtagTimeline = (hashtag: string, { maxId, tags }: Record = {}, done = noOp) => { - return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { +const expandHashtagTimeline = (hashtag: string, { url, maxId, tags }: Record = {}, done = noOp) => { + return expandTimeline(`hashtag:${hashtag}`, url || `/api/v1/timelines/tag/${hashtag}`, url ? {} : { max_id: maxId, any: parseTags(tags, 'any'), all: parseTags(tags, 'all'), diff --git a/app/soapbox/features/account-gallery/index.tsx b/app/soapbox/features/account-gallery/index.tsx index 7cee5c569..86a832a36 100644 --- a/app/soapbox/features/account-gallery/index.tsx +++ b/app/soapbox/features/account-gallery/index.tsx @@ -64,6 +64,7 @@ const AccountGallery = () => { const attachments: ImmutableList = useAppSelector((state) => getAccountGallery(state, accountId as string)); const isLoading = useAppSelector((state) => state.timelines.get(`account:${accountId}:media`)?.isLoading); const hasMore = useAppSelector((state) => state.timelines.get(`account:${accountId}:media`)?.hasMore); + const next = useAppSelector(state => state.timelines.get(`account:${accountId}:media`)?.next); const node = useRef(null); @@ -75,7 +76,7 @@ const AccountGallery = () => { const handleLoadMore = (maxId: string | null) => { if (accountId && accountId !== -1) { - dispatch(expandAccountMediaTimeline(accountId, { maxId })); + dispatch(expandAccountMediaTimeline(accountId, { url: next, maxId })); } }; diff --git a/app/soapbox/features/account-timeline/index.tsx b/app/soapbox/features/account-timeline/index.tsx index 4f8ccc211..e3f5ca3e7 100644 --- a/app/soapbox/features/account-timeline/index.tsx +++ b/app/soapbox/features/account-timeline/index.tsx @@ -40,6 +40,7 @@ const AccountTimeline: React.FC = ({ params, withReplies = fal const patronEnabled = soapboxConfig.getIn(['extensions', 'patron', 'enabled']) === true; const isLoading = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'isLoading']) === true); const hasMore = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'hasMore']) === true); + const next = useAppSelector(state => state.timelines.get(`account:${path}`)?.next); const accountUsername = account?.username || params.username; @@ -69,7 +70,7 @@ const AccountTimeline: React.FC = ({ params, withReplies = fal const handleLoadMore = (maxId: string) => { if (account) { - dispatch(expandAccountTimeline(account.id, { maxId, withReplies })); + dispatch(expandAccountTimeline(account.id, { url: next, maxId, withReplies })); } }; diff --git a/app/soapbox/features/community-timeline/index.tsx b/app/soapbox/features/community-timeline/index.tsx index 3fca53cc5..387297a80 100644 --- a/app/soapbox/features/community-timeline/index.tsx +++ b/app/soapbox/features/community-timeline/index.tsx @@ -5,7 +5,7 @@ import { connectCommunityStream } from 'soapbox/actions/streaming'; import { expandCommunityTimeline } from 'soapbox/actions/timelines'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import { Column } from 'soapbox/components/ui'; -import { useAppDispatch, useSettings } from 'soapbox/hooks'; +import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks'; import Timeline from '../ui/components/timeline'; @@ -19,11 +19,12 @@ const CommunityTimeline = () => { const settings = useSettings(); const onlyMedia = settings.getIn(['community', 'other', 'onlyMedia']); + const next = useAppSelector(state => state.timelines.get('community')?.next); const timelineId = 'community'; const handleLoadMore = (maxId: string) => { - dispatch(expandCommunityTimeline({ maxId, onlyMedia })); + dispatch(expandCommunityTimeline({ url: next, maxId, onlyMedia })); }; const handleRefresh = () => { diff --git a/app/soapbox/features/direct-timeline/index.tsx b/app/soapbox/features/direct-timeline/index.tsx index aef932516..eee31a829 100644 --- a/app/soapbox/features/direct-timeline/index.tsx +++ b/app/soapbox/features/direct-timeline/index.tsx @@ -6,7 +6,7 @@ import { connectDirectStream } from 'soapbox/actions/streaming'; import { expandDirectTimeline } from 'soapbox/actions/timelines'; import AccountSearch from 'soapbox/components/account-search'; import { Column } from 'soapbox/components/ui'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import Timeline from '../ui/components/timeline'; @@ -18,6 +18,7 @@ const messages = defineMessages({ const DirectTimeline = () => { const intl = useIntl(); const dispatch = useAppDispatch(); + const next = useAppSelector(state => state.timelines.get('direct')?.next); useEffect(() => { dispatch(expandDirectTimeline()); @@ -33,7 +34,7 @@ const DirectTimeline = () => { }; const handleLoadMore = (maxId: string) => { - dispatch(expandDirectTimeline({ maxId })); + dispatch(expandDirectTimeline({ url: next, maxId })); }; return ( diff --git a/app/soapbox/features/hashtag-timeline/index.tsx b/app/soapbox/features/hashtag-timeline/index.tsx index e448bef8a..bf906ce01 100644 --- a/app/soapbox/features/hashtag-timeline/index.tsx +++ b/app/soapbox/features/hashtag-timeline/index.tsx @@ -39,6 +39,7 @@ export const HashtagTimeline: React.FC = ({ params }) => { const dispatch = useAppDispatch(); const disconnects = useRef<(() => void)[]>([]); const tag = useAppSelector((state) => state.tags.get(id)); + const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next); // Mastodon supports displaying results from multiple hashtags. // https://github.com/mastodon/mastodon/issues/6359 @@ -89,7 +90,7 @@ export const HashtagTimeline: React.FC = ({ params }) => { }; const handleLoadMore = (maxId: string) => { - dispatch(expandHashtagTimeline(id, { maxId, tags })); + dispatch(expandHashtagTimeline(id, { url: next, maxId, tags })); }; const handleFollow = () => { diff --git a/app/soapbox/features/list-timeline/index.tsx b/app/soapbox/features/list-timeline/index.tsx index 9751f2440..f16acf18b 100644 --- a/app/soapbox/features/list-timeline/index.tsx +++ b/app/soapbox/features/list-timeline/index.tsx @@ -17,6 +17,7 @@ const ListTimeline: React.FC = () => { const { id } = useParams<{ id: string }>(); const list = useAppSelector((state) => state.lists.get(id)); + const next = useAppSelector(state => state.timelines.get(`list:${id}`)?.next); useEffect(() => { dispatch(fetchList(id)); @@ -30,7 +31,7 @@ const ListTimeline: React.FC = () => { }, [id]); const handleLoadMore = (maxId: string) => { - dispatch(expandListTimeline(id, { maxId })); + dispatch(expandListTimeline(id, { url: next, maxId })); }; const handleEditClick = () => { diff --git a/app/soapbox/features/public-timeline/index.tsx b/app/soapbox/features/public-timeline/index.tsx index 8f96e432d..cad8cd7f6 100644 --- a/app/soapbox/features/public-timeline/index.tsx +++ b/app/soapbox/features/public-timeline/index.tsx @@ -7,7 +7,7 @@ import { connectPublicStream } from 'soapbox/actions/streaming'; import { expandPublicTimeline } from 'soapbox/actions/timelines'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import { Accordion, Column } from 'soapbox/components/ui'; -import { useAppDispatch, useInstance, useSettings } from 'soapbox/hooks'; +import { useAppSelector, useAppDispatch, useInstance, useSettings } from 'soapbox/hooks'; import PinnedHostsPicker from '../remote-timeline/components/pinned-hosts-picker'; import Timeline from '../ui/components/timeline'; @@ -24,6 +24,7 @@ const CommunityTimeline = () => { const instance = useInstance(); const settings = useSettings(); const onlyMedia = settings.getIn(['public', 'other', 'onlyMedia']); + const next = useAppSelector(state => state.timelines.get('public')?.next); const timelineId = 'public'; @@ -39,7 +40,7 @@ const CommunityTimeline = () => { }; const handleLoadMore = (maxId: string) => { - dispatch(expandPublicTimeline({ maxId, onlyMedia })); + dispatch(expandPublicTimeline({ url: next, maxId, onlyMedia })); }; const handleRefresh = () => { diff --git a/app/soapbox/features/remote-timeline/index.tsx b/app/soapbox/features/remote-timeline/index.tsx index 3283078af..b0afd38a8 100644 --- a/app/soapbox/features/remote-timeline/index.tsx +++ b/app/soapbox/features/remote-timeline/index.tsx @@ -6,7 +6,7 @@ import { connectRemoteStream } from 'soapbox/actions/streaming'; import { expandRemoteTimeline } from 'soapbox/actions/timelines'; import IconButton from 'soapbox/components/icon-button'; import { Column, HStack, Text } from 'soapbox/components/ui'; -import { useAppDispatch, useSettings } from 'soapbox/hooks'; +import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks'; import Timeline from '../ui/components/timeline'; @@ -30,6 +30,7 @@ const RemoteTimeline: React.FC = ({ params }) => { const timelineId = 'remote'; const onlyMedia = !!settings.getIn(['remote', 'other', 'onlyMedia']); + const next = useAppSelector(state => state.timelines.get('remote')?.next); const pinned: boolean = (settings.getIn(['remote_timeline', 'pinnedHosts']) as any).includes(instance); @@ -44,7 +45,7 @@ const RemoteTimeline: React.FC = ({ params }) => { }; const handleLoadMore = (maxId: string) => { - dispatch(expandRemoteTimeline(instance, { maxId, onlyMedia })); + dispatch(expandRemoteTimeline(instance, { url: next, maxId, onlyMedia })); }; useEffect(() => { From 2796726cad222df589bd236573d560e076a89741 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 19 Jun 2023 16:49:42 -0500 Subject: [PATCH 7/7] utils: pick only needed fields --- app/soapbox/utils/accounts.ts | 11 +++++------ app/soapbox/utils/ads.ts | 2 +- app/soapbox/utils/status.ts | 27 +++++++++++++++++---------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/soapbox/utils/accounts.ts b/app/soapbox/utils/accounts.ts index ef7c446a5..d8d946f9d 100644 --- a/app/soapbox/utils/accounts.ts +++ b/app/soapbox/utils/accounts.ts @@ -1,7 +1,6 @@ import type { Account } from 'soapbox/schemas'; -import type { Account as AccountEntity } from 'soapbox/types/entities'; -const getDomainFromURL = (account: AccountEntity): string => { +const getDomainFromURL = (account: Pick): string => { try { const url = account.url; return new URL(url).host; @@ -10,12 +9,12 @@ const getDomainFromURL = (account: AccountEntity): string => { } }; -export const getDomain = (account: AccountEntity): string => { +export const getDomain = (account: Pick): string => { const domain = account.acct.split('@')[1]; return domain ? domain : getDomainFromURL(account); }; -export const getBaseURL = (account: AccountEntity): string => { +export const getBaseURL = (account: Pick): string => { try { return new URL(account.url).origin; } catch { @@ -27,12 +26,12 @@ export const getAcct = (account: Pick, displayFqn: bool displayFqn === true ? account.fqn : account.acct ); -export const isLocal = (account: AccountEntity | Account): boolean => { +export const isLocal = (account: Pick): boolean => { const domain: string = account.acct.split('@')[1]; return domain === undefined ? true : false; }; -export const isRemote = (account: AccountEntity): boolean => !isLocal(account); +export const isRemote = (account: Pick): boolean => !isLocal(account); /** Default header filenames from various backends */ const DEFAULT_HEADERS = [ diff --git a/app/soapbox/utils/ads.ts b/app/soapbox/utils/ads.ts index b59cf430b..ed2bf0cad 100644 --- a/app/soapbox/utils/ads.ts +++ b/app/soapbox/utils/ads.ts @@ -4,7 +4,7 @@ import type { Ad } from 'soapbox/schemas'; const AD_EXPIRY_THRESHOLD = 5 * 60 * 1000; /** Whether the ad is expired or about to expire. */ -const isExpired = (ad: Ad, threshold = AD_EXPIRY_THRESHOLD): boolean => { +const isExpired = (ad: Pick, threshold = AD_EXPIRY_THRESHOLD): boolean => { if (ad.expires_at) { const now = new Date(); return now.getTime() > (new Date(ad.expires_at).getTime() - threshold); diff --git a/app/soapbox/utils/status.ts b/app/soapbox/utils/status.ts index 74c43e071..09593facc 100644 --- a/app/soapbox/utils/status.ts +++ b/app/soapbox/utils/status.ts @@ -1,10 +1,13 @@ import { isIntegerId } from 'soapbox/utils/numbers'; import type { IntlShape } from 'react-intl'; -import type { Status as StatusEntity } from 'soapbox/types/entities'; +import type { Status } from 'soapbox/types/entities'; /** Get the initial visibility of media attachments from user settings. */ -export const defaultMediaVisibility = (status: StatusEntity | undefined | null, displayMedia: string): boolean => { +export const defaultMediaVisibility = ( + status: Pick | undefined | null, + displayMedia: string, +): boolean => { if (!status) return false; if (status.reblog && typeof status.reblog === 'object') { @@ -21,7 +24,7 @@ export const defaultMediaVisibility = (status: StatusEntity | undefined | null, }; /** Grab the first external link from a status. */ -export const getFirstExternalLink = (status: StatusEntity): HTMLAnchorElement | null => { +export const getFirstExternalLink = (status: Pick): HTMLAnchorElement | null => { try { // Pulled from Pleroma's media parser const selector = 'a:not(.mention,.hashtag,.attachment,[rel~="tag"])'; @@ -34,18 +37,22 @@ export const getFirstExternalLink = (status: StatusEntity): HTMLAnchorElement | }; /** Whether the status is expected to have a Card after it loads. */ -export const shouldHaveCard = (status: StatusEntity): boolean => { +export const shouldHaveCard = (status: Pick): boolean => { return Boolean(getFirstExternalLink(status)); }; /** Whether the media IDs on this status have integer IDs (opposed to FlakeIds). */ // https://gitlab.com/soapbox-pub/soapbox/-/merge_requests/1087 -export const hasIntegerMediaIds = (status: StatusEntity): boolean => { +export const hasIntegerMediaIds = (status: Pick): boolean => { return status.media_attachments.some(({ id }) => isIntegerId(id)); }; /** Sanitize status text for use with screen readers. */ -export const textForScreenReader = (intl: IntlShape, status: StatusEntity, rebloggedByText?: string): string => { +export const textForScreenReader = ( + intl: IntlShape, + status: Pick, + rebloggedByText?: string, +): string => { const { account } = status; if (!account || typeof account !== 'object') return ''; @@ -55,7 +62,7 @@ export const textForScreenReader = (intl: IntlShape, status: StatusEntity, reblo displayName.length === 0 ? account.acct.split('@')[0] : displayName, status.spoiler_text && status.hidden ? status.spoiler_text : status.search_index.slice(status.spoiler_text.length), intl.formatDate(status.created_at, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), - status.getIn(['account', 'acct']), + account.acct, ]; if (rebloggedByText) { @@ -68,12 +75,12 @@ export const textForScreenReader = (intl: IntlShape, status: StatusEntity, reblo /** Get reblogged status if any, otherwise return the original status. */ // @ts-ignore The type seems right, but TS doesn't like it. export const getActualStatus: { - (status: StatusEntity): StatusEntity + >(status: T): T (status: undefined): undefined (status: null): null -} = (status) => { +} = >(status: T | null | undefined) => { if (status?.reblog && typeof status?.reblog === 'object') { - return status.reblog as StatusEntity; + return status.reblog as Status; } else { return status; }