From 324a625af9eb54dd1687885f55e0d9dea6f63fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:47:34 +0100 Subject: [PATCH] nicolium: remove timelines actions/reducer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/admin.ts | 5 +- packages/nicolium/src/actions/statuses.ts | 1 - packages/nicolium/src/actions/timelines.ts | 512 ------------------ .../api/hooks/streaming/use-user-stream.ts | 3 - packages/nicolium/src/features/ui/index.tsx | 3 - .../src/modals/report-modal/index.tsx | 10 +- .../src/pages/accounts/account-timeline.tsx | 15 +- .../src/pages/timelines/circle-timeline.tsx | 9 +- packages/nicolium/src/reducers/timelines.ts | 266 --------- packages/nicolium/src/utils/timelines.ts | 26 - 10 files changed, 7 insertions(+), 843 deletions(-) delete mode 100644 packages/nicolium/src/actions/timelines.ts delete mode 100644 packages/nicolium/src/reducers/timelines.ts delete mode 100644 packages/nicolium/src/utils/timelines.ts diff --git a/packages/nicolium/src/actions/admin.ts b/packages/nicolium/src/actions/admin.ts index 0f5f1b13d..0cb1e8ab6 100644 --- a/packages/nicolium/src/actions/admin.ts +++ b/packages/nicolium/src/actions/admin.ts @@ -4,10 +4,9 @@ import { queryClient } from '@/queries/client'; import { queryKeys } from '@/queries/keys'; import { useComposeStore } from '@/stores/compose'; import { useModalsStore } from '@/stores/modals'; +import { useTimelinesStore } from '@/stores/timelines'; import { filterBadges, getTagDiff } from '@/utils/badges'; -import { deleteFromTimelines } from './timelines'; - import type { AppDispatch, RootState } from '@/store'; import type { PleromaConfig } from 'pl-api'; @@ -73,7 +72,7 @@ const deleteStatus = (statusId: string) => (dispatch: AppDispatch, getState: () getClient(getState) .admin.statuses.deleteStatus(statusId) .then(() => { - dispatch(deleteFromTimelines(statusId)); + useTimelinesStore.getState().actions.deleteStatus(statusId); return { statusId }; }); diff --git a/packages/nicolium/src/actions/statuses.ts b/packages/nicolium/src/actions/statuses.ts index 23a7e48dc..5bfc84192 100644 --- a/packages/nicolium/src/actions/statuses.ts +++ b/packages/nicolium/src/actions/statuses.ts @@ -13,7 +13,6 @@ import { isLoggedIn } from '@/utils/auth'; import { shouldHaveCard } from '@/utils/status'; import { importEntities } from './importer'; -import { deleteFromTimelines } from './timelines'; import type { NormalizedStatus as Status } from '@/normalizers/status'; import type { AppDispatch, RootState } from '@/store'; diff --git a/packages/nicolium/src/actions/timelines.ts b/packages/nicolium/src/actions/timelines.ts deleted file mode 100644 index 35725d5cf..000000000 --- a/packages/nicolium/src/actions/timelines.ts +++ /dev/null @@ -1,512 +0,0 @@ -import { getLocale } from '@/actions/settings'; -import { getClient } from '@/api'; -import { queryClient } from '@/queries/client'; -import { queryKeys } from '@/queries/keys'; -import { findStatuses } from '@/queries/statuses/use-status'; -import { useComposeStore } from '@/stores/compose'; -import { useContextStore } from '@/stores/contexts'; -import { usePendingStatusesStore } from '@/stores/pending-statuses'; -import { useSettingsStore } from '@/stores/settings'; -import { shouldFilter } from '@/utils/timelines'; - -import { importEntities } from './importer'; - -import type { AppDispatch, RootState } from '@/store'; -import type { - Account as BaseAccount, - AntennaTimelineParams, - GetAccountStatusesParams, - GetCircleStatusesParams, - GroupTimelineParams, - HashtagTimelineParams, - HomeTimelineParams, - ListTimelineParams, - PaginatedResponse, - PublicTimelineParams, - Status as BaseStatus, - LinkTimelineParams, -} from 'pl-api'; - -const TIMELINE_UPDATE = 'TIMELINE_UPDATE' as const; -const TIMELINE_DELETE = 'TIMELINE_DELETE' as const; -const TIMELINE_CLEAR = 'TIMELINE_CLEAR' as const; -const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE' as const; -const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE' as const; -const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP' as const; - -const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST' as const; -const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS' as const; -const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL' as const; - -const MAX_QUEUED_ITEMS = 40; - -const processTimelineUpdate = - (timeline: string, status: BaseStatus) => (dispatch: AppDispatch, getState: () => RootState) => { - const me = getState().me; - const ownStatus = status.account?.id === me; - - const hasPendingStatuses = Object.keys(usePendingStatusesStore.getState().statuses).length > 0; - - const columnSettings = useSettingsStore.getState().settings.timelines[timeline]; - const shouldSkipQueue = shouldFilter( - { - in_reply_to_id: status.in_reply_to_id, - visibility: status.visibility, - reblog_id: status.reblog?.id ?? null, - }, - columnSettings, - ); - - if (ownStatus && hasPendingStatuses) { - // WebSockets push statuses without the Idempotency-Key, - // so if we have pending statuses, don't import it from here. - // We implement optimistic non-blocking statuses. - return; - } - - if (shouldSkipQueue) { - dispatch(updateTimeline(timeline, status.id)); - } else { - dispatch(updateTimelineQueue(timeline, status.id)); - } - }; - -const updateTimeline = (timeline: string, statusId: string) => ({ - type: TIMELINE_UPDATE, - timeline, - statusId, -}); - -const updateTimelineQueue = (timeline: string, statusId: string) => ({ - // if (typeof accept === 'function' && !accept(status)) { - // return; - // } - type: TIMELINE_UPDATE_QUEUE, - timeline, - statusId, -}); - -interface TimelineDequeueAction { - type: typeof TIMELINE_DEQUEUE; - timeline: string; -} - -const dequeueTimeline = - (timelineId: string, expandFunc?: () => void) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const queuedCount = state.timelines[timelineId]?.totalQueuedItemsCount || 0; - - if (queuedCount <= 0) return; - - if (queuedCount <= MAX_QUEUED_ITEMS) { - dispatch({ type: TIMELINE_DEQUEUE, timeline: timelineId }); - return; - } - - if (typeof expandFunc === 'function') { - dispatch(clearTimeline(timelineId)); - expandFunc(); - } else if (timelineId === 'home') { - dispatch(clearTimeline(timelineId)); - dispatch(fetchHomeTimeline()); - } else if (timelineId === 'public:local') { - dispatch(clearTimeline(timelineId)); - dispatch(fetchPublicTimeline({ local: true })); - } - }; - -interface TimelineDeleteAction { - type: typeof TIMELINE_DELETE; - statusId: string; - accountId: string; - references: Array<[string, string]>; - reblogOf: string | null; -} - -const deleteFromTimelines = (statusId: string) => (dispatch: AppDispatch) => { - const status = queryClient.getQueryData(queryKeys.statuses.show(statusId)); - const accountId = status?.account_id ?? ''; - const references: Array<[string, string]> = findStatuses((s) => s.reblog_id === statusId).map( - ([id, s]) => [id, s.account_id], - ); - const reblogOf = status?.reblog_id ?? null; - - useContextStore.getState().actions.deleteStatuses([statusId]); - useComposeStore.getState().actions.handleTimelineDelete(statusId); - - // Remove statuses from RQ cache - references.forEach(([refId]) => - queryClient.removeQueries({ queryKey: queryKeys.statuses.show(refId) }), - ); - queryClient.removeQueries({ queryKey: queryKeys.statuses.show(statusId) }); - - dispatch({ - type: TIMELINE_DELETE, - statusId, - accountId, - references, - reblogOf, - }); -}; - -const clearTimeline = (timeline: string) => ({ type: TIMELINE_CLEAR, timeline }); - -const noOp = () => {}; - -const parseTags = (tags: Record = {}, mode: 'any' | 'all' | 'none') => - (tags[mode] || []).map((tag) => tag.value); - -const deduplicateStatuses = (statuses: Array) => { - const deduplicatedStatuses: Array }> = []; - - for (const status of statuses) { - const reblogged = - status.reblog && - deduplicatedStatuses.find( - (deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.reblog?.id, - ); - - if (reblogged) { - reblogged.accounts.push(status.account); - reblogged.id += ':' + status.id; - } else if ( - !deduplicatedStatuses.some( - (deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.id, - ) - ) { - deduplicatedStatuses.push({ accounts: [status.account], ...status }); - } - } - - return deduplicatedStatuses; -}; - -const handleTimelineExpand = - ( - timelineId: string, - fn: Promise>, - done = noOp, - onError?: (error: any) => void, - ) => - async (dispatch: AppDispatch) => { - dispatch(expandTimelineRequest(timelineId)); - - try { - const response = await fn; - - importEntities({ statuses: response.items }); - - const statuses = deduplicateStatuses(response.items); - importEntities({ statuses: statuses.filter((status) => status.accounts) }); - - dispatch( - expandTimelineSuccess( - timelineId, - statuses, - response.next, - response.previous, - response.partial, - ), - ); - done(); - } catch (error) { - dispatch(expandTimelineFail(timelineId, error)); - done(); - onError?.(error); - } - }; - -const fetchHomeTimeline = - (expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - - const params: HomeTimelineParams = {}; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines.home?.isLoading) return; - - const fn = - (expand && state.timelines.home?.next?.()) || getClient(state).timelines.homeTimeline(params); - - return dispatch(handleTimelineExpand('home', fn, done)); - }; - -const fetchPublicTimeline = - ( - { onlyMedia, local, instance }: Record = {}, - expand = false, - done = noOp, - onError = noOp, - ) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `${instance ? 'remote' : 'public'}${local ? ':local' : ''}${onlyMedia ? ':media' : ''}${instance ? `:${instance}` : ''}`; - - const params: PublicTimelineParams = { - only_media: onlyMedia, - local: instance ? false : local, - instance, - }; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.publicTimeline(params); - - return dispatch(handleTimelineExpand(timelineId, fn, done, onError)); - }; - -const fetchBubbleTimeline = - ({ onlyMedia }: Record = {}, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `bubble${onlyMedia ? ':media' : ''}`; - - const params: PublicTimelineParams = { only_media: onlyMedia }; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.bubbleTimeline(params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchWrenchedTimeline = - ({ onlyMedia }: Record = {}, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `wrenched${onlyMedia ? ':media' : ''}`; - - const params: PublicTimelineParams = { only_media: onlyMedia }; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.wrenchedTimeline(params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchAccountTimeline = - ( - accountId: string, - { exclude_replies, pinned, only_media, limit }: Record = {}, - expand = false, - done = noOp, - ) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `account:${accountId}${!exclude_replies ? ':with_replies' : ''}${pinned ? ':pinned' : only_media ? ':media' : ''}`; - - const params: GetAccountStatusesParams = { exclude_replies, pinned, only_media, limit }; - if (pinned || only_media) params.with_muted = true; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (!expand && state.timelines[timelineId]?.loaded) return; - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).accounts.getAccountStatuses(accountId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchListTimeline = - (listId: string, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `list:${listId}`; - - const params: ListTimelineParams = {}; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.listTimeline(listId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchCircleTimeline = - (circleId: string, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `circle:${circleId}`; - - const params: GetCircleStatusesParams = {}; - // if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).circles.getCircleStatuses(circleId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchAntennaTimeline = - (antennaId: string, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `antenna:${antennaId}`; - - const params: AntennaTimelineParams = {}; - // if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.antennaTimeline(antennaId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchGroupTimeline = - (groupId: string, { only_media, limit }: Record = {}, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `group:${groupId}${only_media ? ':media' : ''}`; - - const params: GroupTimelineParams = { only_media, limit }; - if (only_media) params.with_muted = true; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.groupTimeline(groupId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchHashtagTimeline = - (hashtag: string, { tags }: Record = {}, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `hashtag:${hashtag}`; - - const params: HashtagTimelineParams = { - any: parseTags(tags, 'any'), - all: parseTags(tags, 'all'), - none: parseTags(tags, 'none'), - }; - - if (expand && state.timelines[timelineId]?.isLoading) return; - - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.hashtagTimeline(hashtag, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchLinkTimeline = - (url: string, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `link:${url}`; - - const params: LinkTimelineParams = {}; - - if (expand && state.timelines[timelineId]?.isLoading) return; - - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.linkTimeline(url, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const expandTimelineRequest = (timeline: string) => ({ - type: TIMELINE_EXPAND_REQUEST, - timeline, -}); - -const expandTimelineSuccess = ( - timeline: string, - statuses: Array, - next: (() => Promise>) | null, - prev: (() => Promise>) | null, - partial: boolean, -) => ({ - type: TIMELINE_EXPAND_SUCCESS, - timeline, - statuses, - next, - prev, - partial, -}); - -const expandTimelineFail = (timeline: string, error: unknown) => ({ - type: TIMELINE_EXPAND_FAIL, - timeline, - error, -}); - -const scrollTopTimeline = (timeline: string, top: boolean) => ({ - type: TIMELINE_SCROLL_TOP, - timeline, - top, -}); - -// TODO: other actions -type TimelineAction = - | ReturnType - | TimelineDeleteAction - | ReturnType - | ReturnType - | TimelineDequeueAction - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - -export { - TIMELINE_UPDATE, - TIMELINE_DELETE, - TIMELINE_CLEAR, - TIMELINE_UPDATE_QUEUE, - TIMELINE_DEQUEUE, - TIMELINE_SCROLL_TOP, - TIMELINE_EXPAND_REQUEST, - TIMELINE_EXPAND_SUCCESS, - TIMELINE_EXPAND_FAIL, - MAX_QUEUED_ITEMS, - processTimelineUpdate, - dequeueTimeline, - deleteFromTimelines, - clearTimeline, - fetchHomeTimeline, - fetchPublicTimeline, - fetchBubbleTimeline, - fetchWrenchedTimeline, - fetchAccountTimeline, - fetchListTimeline, - fetchCircleTimeline, - fetchAntennaTimeline, - fetchGroupTimeline, - fetchHashtagTimeline, - fetchLinkTimeline, - expandTimelineSuccess, - scrollTopTimeline, - type TimelineAction, -}; diff --git a/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts index d7d8fdb3b..d46fe8b82 100644 --- a/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts +++ b/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { importEntities } from '@/actions/importer'; -import { deleteFromTimelines, processTimelineUpdate } from '@/actions/timelines'; import { useStatContext } from '@/contexts/stat-context'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useLoggedIn } from '@/hooks/use-logged-in'; @@ -113,7 +112,6 @@ const useUserStream = () => { switch (event.event) { case 'update': { const timelineId = getTimelineFromStream(event.stream); - dispatch(processTimelineUpdate(getTimelineFromStream(event.stream), event.payload)); importEntities({ statuses: [event.payload] }); useTimelinesStore.getState().actions.receiveStreamingStatus(timelineId, event.payload); break; @@ -122,7 +120,6 @@ const useUserStream = () => { importEntities({ statuses: [event.payload] }); break; case 'delete': - dispatch(deleteFromTimelines(event.payload)); useTimelinesStore.getState().actions.deleteStatus(event.payload); break; case 'notification': diff --git a/packages/nicolium/src/features/ui/index.tsx b/packages/nicolium/src/features/ui/index.tsx index 8153538fa..ca314889e 100644 --- a/packages/nicolium/src/features/ui/index.tsx +++ b/packages/nicolium/src/features/ui/index.tsx @@ -5,7 +5,6 @@ import { Toaster } from 'react-hot-toast'; import { fetchConfig } from '@/actions/admin'; import { register as registerPushNotifications } from '@/actions/push-notifications/registerer'; -import { fetchHomeTimeline } from '@/actions/timelines'; import { useUserStream } from '@/api/hooks/streaming/use-user-stream'; import SidebarNavigation from '@/components/navigation/sidebar-navigation'; import ThumbNavigation from '@/components/navigation/thumb-navigation'; @@ -91,8 +90,6 @@ const UI: React.FC = React.memo(() => { prefetchCustomEmojis(client); - dispatch(fetchHomeTimeline()); - if (account.is_admin && features.pleromaAdminAccounts) { dispatch(fetchConfig()); } diff --git a/packages/nicolium/src/modals/report-modal/index.tsx b/packages/nicolium/src/modals/report-modal/index.tsx index 85a3da6b1..304dd577b 100644 --- a/packages/nicolium/src/modals/report-modal/index.tsx +++ b/packages/nicolium/src/modals/report-modal/index.tsx @@ -1,8 +1,7 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { submitReport, ReportableEntities } from '@/actions/reports'; -import { fetchAccountTimeline } from '@/actions/timelines'; import AttachmentThumbs from '@/components/media/attachment-thumbs'; import StatusContent from '@/components/statuses/status-content'; import Modal from '@/components/ui/modal'; @@ -15,6 +14,7 @@ import { useInstance } from '@/hooks/use-instance'; import { useAccount } from '@/queries/accounts/use-account'; import { useBlockAccountMutation } from '@/queries/accounts/use-relationship'; import { useMinimalStatus } from '@/queries/statuses/use-status'; +import { useAccountTimeline } from '@/queries/timelines/use-timelines'; import ConfirmationStep from './steps/confirmation-step'; import OtherActionsStep from './steps/other-actions-step'; @@ -230,11 +230,7 @@ const ReportModal: React.FC = ({ } }, [currentStep]); - useEffect(() => { - if (account?.id) { - dispatch(fetchAccountTimeline(account.id, { exclude_replies: false })); - } - }, [account?.id]); + useAccountTimeline(accountId, { exclude_replies: false }); if (!account) { return null; diff --git a/packages/nicolium/src/pages/accounts/account-timeline.tsx b/packages/nicolium/src/pages/accounts/account-timeline.tsx index 46df165e3..845288d3f 100644 --- a/packages/nicolium/src/pages/accounts/account-timeline.tsx +++ b/packages/nicolium/src/pages/accounts/account-timeline.tsx @@ -1,14 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchAccountTimeline } from '@/actions/timelines'; import { AccountTimelineColumn } from '@/columns/timeline'; import MissingIndicator from '@/components/missing-indicator'; import Card, { CardBody } from '@/components/ui/card'; import Spinner from '@/components/ui/spinner'; import Text from '@/components/ui/text'; import { profileRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFeatures } from '@/hooks/use-features'; import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; import { usePinnedStatuses } from '@/queries/status-lists/use-pinned-statuses'; @@ -17,7 +15,6 @@ const AccountTimelinePage: React.FC = () => { const { username } = profileRoute.useParams(); const { with_replies: withReplies = false } = profileRoute.useSearch(); - const dispatch = useAppDispatch(); const features = useFeatures(); const { data: account, isPending } = useAccountLookup(username); @@ -28,16 +25,6 @@ const AccountTimelinePage: React.FC = () => { const accountUsername = account?.username ?? username; - useEffect(() => { - if (account) { - dispatch(fetchAccountTimeline(account.id, { exclude_replies: !withReplies })); - - if (!withReplies) { - dispatch(fetchAccountTimeline(account.id, { pinned: true })); - } - } - }, [account?.id, withReplies]); - if (!account && isPending) { return ; } else if (!account) { diff --git a/packages/nicolium/src/pages/timelines/circle-timeline.tsx b/packages/nicolium/src/pages/timelines/circle-timeline.tsx index 0c71a1c16..34ac50cd9 100644 --- a/packages/nicolium/src/pages/timelines/circle-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/circle-timeline.tsx @@ -1,8 +1,7 @@ import { useNavigate } from '@tanstack/react-router'; -import React, { useEffect } from 'react'; +import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { fetchCircleTimeline } from '@/actions/timelines'; import { CircleTimelineColumn } from '@/columns/timeline'; import DropdownMenu from '@/components/dropdown-menu'; import MissingIndicator from '@/components/missing-indicator'; @@ -10,7 +9,6 @@ import Button from '@/components/ui/button'; import Column from '@/components/ui/column'; import Spinner from '@/components/ui/spinner'; import { circleTimelineRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useCircle, useDeleteCircle } from '@/queries/accounts/use-circles'; import { useModalsActions } from '@/stores/modals'; @@ -29,17 +27,12 @@ const CircleTimelinePage: React.FC = () => { const { circleId } = circleTimelineRoute.useParams(); const intl = useIntl(); - const dispatch = useAppDispatch(); const { openModal } = useModalsActions(); const navigate = useNavigate(); const { data: circle, isFetching } = useCircle(circleId); const { mutate: deleteCircle } = useDeleteCircle(); - useEffect(() => { - dispatch(fetchCircleTimeline(circleId)); - }, [circleId]); - const handleEditClick = () => { openModal('CIRCLE_EDITOR', { circleId }); }; diff --git a/packages/nicolium/src/reducers/timelines.ts b/packages/nicolium/src/reducers/timelines.ts deleted file mode 100644 index 04f303cf3..000000000 --- a/packages/nicolium/src/reducers/timelines.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { create } from 'mutative'; - -import { - TIMELINE_UPDATE, - TIMELINE_DELETE, - TIMELINE_CLEAR, - TIMELINE_EXPAND_SUCCESS, - TIMELINE_EXPAND_REQUEST, - TIMELINE_EXPAND_FAIL, - TIMELINE_UPDATE_QUEUE, - TIMELINE_DEQUEUE, - MAX_QUEUED_ITEMS, - TIMELINE_SCROLL_TOP, - type TimelineAction, -} from '@/actions/timelines'; - -import type { PaginatedResponse, Status as BaseStatus } from 'pl-api'; - -type ImportPosition = 'start' | 'end'; - -const TRUNCATE_LIMIT = 40; -const TRUNCATE_SIZE = 20; - -interface Timeline { - unread: number; - top: boolean; - isLoading: boolean; - hasMore: boolean; - next: (() => Promise>) | null; - prev: (() => Promise>) | null; - items: Array; - queuedItems: Array; //max= MAX_QUEUED_ITEMS - totalQueuedItemsCount: number; //used for queuedItems overflow for MAX_QUEUED_ITEMS+ - loadingFailed: boolean; - isPartial: boolean; - loaded: boolean; -} - -const newTimeline = (): Timeline => ({ - unread: 0, - top: true, - isLoading: false, - hasMore: true, - next: null, - prev: null, - items: [], - queuedItems: [], //max= MAX_QUEUED_ITEMS - totalQueuedItemsCount: 0, //used for queuedItems overflow for MAX_QUEUED_ITEMS+ - loadingFailed: false, - isPartial: false, - loaded: false, -}); - -const initialState: State = {}; - -type State = Record; - -const getStatusIds = (statuses: Array> = []) => - statuses.map((status) => status.id); - -const mergeStatusIds = (oldIds: Array, newIds: Array) => [ - ...new Set([...newIds, ...oldIds]), -]; - -const addStatusId = (oldIds = Array(), newId: string) => mergeStatusIds(oldIds, [newId]); - -// Like `take`, but only if the collection's size exceeds truncateLimit -const truncate = (items: Array, truncateLimit: number, newSize: number) => - items.length > truncateLimit ? items.slice(0, newSize) : items; - -const truncateIds = (items: Array) => truncate(items, TRUNCATE_LIMIT, TRUNCATE_SIZE); - -const updateTimeline = ( - state: State, - timelineId: string, - updater: (timeline: Timeline) => void, -) => { - state[timelineId] = state[timelineId] || newTimeline(); - updater(state[timelineId]); -}; - -const setLoading = (state: State, timelineId: string, loading: boolean) => { - updateTimeline(state, timelineId, (timeline) => { - timeline.isLoading = loading; - }); -}; - -// Keep track of when a timeline failed to load -const setFailed = (state: State, timelineId: string, failed: boolean) => { - updateTimeline(state, timelineId, (timeline) => { - timeline.loadingFailed = failed; - }); -}; - -const expandNormalizedTimeline = ( - state: State, - timelineId: string, - statuses: Array, - next: (() => Promise>) | null, - prev: (() => Promise>) | null, - isPartial: boolean, - pos: ImportPosition = 'end', -) => { - const newIds = getStatusIds(statuses); - - updateTimeline(state, timelineId, (timeline) => { - timeline.isLoading = false; - timeline.loadingFailed = false; - timeline.isPartial = isPartial; - timeline.next = next; - timeline.prev = prev; - timeline.loaded = true; - - if (!next) timeline.hasMore = false; - - if (newIds.length) { - if (pos === 'end') { - timeline.items = mergeStatusIds(newIds, timeline.items); - } else { - timeline.items = mergeStatusIds(timeline.items, newIds); - } - } - }); -}; - -const appendStatus = (state: State, timelineId: string, statusId: string) => { - const top = state[timelineId]?.top; - const oldIds = state[timelineId]?.items || []; - const unread = state[timelineId]?.unread || 0; - - if (oldIds.includes(statusId) || state[timelineId]?.queuedItems.includes(statusId)) return state; - - const newIds = addStatusId(oldIds, statusId); - - updateTimeline(state, timelineId, (timeline) => { - if (top) { - // For performance, truncate items if user is scrolled to the top - timeline.items = truncateIds(newIds); - } else { - timeline.unread = unread + 1; - timeline.items = newIds; - } - }); -}; - -const updateTimelineQueue = (state: State, timelineId: string, statusId: string) => { - updateTimeline(state, timelineId, (timeline) => { - const { - queuedItems: queuedIds, - items: listedIds, - totalQueuedItemsCount: queuedCount, - } = timeline; - - if (queuedIds.includes(statusId)) return; - if (listedIds.includes(statusId)) return; - - timeline.totalQueuedItemsCount = queuedCount + 1; - timeline.queuedItems = addStatusId(queuedIds, statusId).slice(0, MAX_QUEUED_ITEMS); - }); -}; - -const shouldDelete = (timelineId: string, excludeAccount: string | null) => { - if (!excludeAccount) return true; - if (timelineId === `account:${excludeAccount}`) return false; - if (timelineId.startsWith(`account:${excludeAccount}:`)) return false; - return true; -}; - -const deleteStatus = ( - state: State, - statusId: string, - references: Array<[string]> | Array<[string, string]>, - excludeAccount: string | null, -) => { - for (const timelineId in state) { - if (shouldDelete(timelineId, excludeAccount)) { - state[timelineId].items = state[timelineId].items.filter((id) => id !== statusId); - state[timelineId].queuedItems = state[timelineId].queuedItems.filter((id) => id !== statusId); - } - } - - // Remove reblogs of deleted status - references.forEach((ref) => { - deleteStatus(state, ref[0], [], excludeAccount); - }); -}; - -const clearTimeline = (state: State, timelineId: string) => { - state[timelineId] = newTimeline(); -}; - -const updateTop = (state: State, timelineId: string, top: boolean) => { - updateTimeline(state, timelineId, (timeline) => { - if (top) timeline.unread = 0; - timeline.top = top; - }); -}; - -const timelineDequeue = (state: State, timelineId: string) => { - updateTimeline(state, timelineId, (timeline) => { - const top = timeline.top; - - const queuedIds = timeline.queuedItems; - - const newIds = mergeStatusIds(timeline.items, queuedIds); - timeline.items = top ? truncateIds(newIds) : newIds; - - timeline.queuedItems = []; - timeline.totalQueuedItemsCount = 0; - }); -}; - -const handleExpandFail = (state: State, timelineId: string) => { - setLoading(state, timelineId, false); - setFailed(state, timelineId, true); -}; - -const timelines = (state: State = initialState, action: TimelineAction): State => { - switch (action.type) { - case TIMELINE_EXPAND_REQUEST: - return create(state, (draft) => { - setLoading(draft, action.timeline, true); - }); - case TIMELINE_EXPAND_FAIL: - return create(state, (draft) => { - handleExpandFail(draft, action.timeline); - }); - case TIMELINE_EXPAND_SUCCESS: - return create(state, (draft) => { - expandNormalizedTimeline( - draft, - action.timeline, - action.statuses, - action.next, - action.prev, - action.partial, - ); - }); - case TIMELINE_UPDATE: - return create(state, (draft) => appendStatus(draft, action.timeline, action.statusId)); - case TIMELINE_UPDATE_QUEUE: - return create(state, (draft) => { - updateTimelineQueue(draft, action.timeline, action.statusId); - }); - case TIMELINE_DEQUEUE: - return create(state, (draft) => { - timelineDequeue(draft, action.timeline); - }); - case TIMELINE_DELETE: - return create(state, (draft) => { - deleteStatus(draft, action.statusId, action.references, action.reblogOf); - }); - case TIMELINE_CLEAR: - return create(state, (draft) => { - clearTimeline(draft, action.timeline); - }); - case TIMELINE_SCROLL_TOP: - return create(state, (draft) => { - updateTop(draft, action.timeline, action.top); - }); - default: - return state; - } -}; - -export { timelines as default }; diff --git a/packages/nicolium/src/utils/timelines.ts b/packages/nicolium/src/utils/timelines.ts deleted file mode 100644 index 2ab93a8fd..000000000 --- a/packages/nicolium/src/utils/timelines.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { NormalizedStatus as Status } from '@/normalizers/status'; -import type { Settings } from '@/schemas/frontend-settings'; - -const shouldFilter = ( - status: Pick, - columnSettings: Settings['timelines'][''], -) => { - const fallback = { - reblog: true, - reply: true, - direct: false, - }; - - const shows = { - reblog: status.reblog_id !== null, - reply: status.in_reply_to_id !== null, - direct: status.visibility === 'direct', - }; - - return Object.entries(shows).some( - ([key, value]) => - !(columnSettings?.shows || fallback)[key as 'reblog' | 'reply' | 'direct'] && value, - ); -}; - -export { shouldFilter };