diff --git a/packages/pl-fe/src/actions/timelines.ts b/packages/pl-fe/src/actions/timelines.ts index 8e83879ba..4409eff02 100644 --- a/packages/pl-fe/src/actions/timelines.ts +++ b/packages/pl-fe/src/actions/timelines.ts @@ -76,7 +76,7 @@ interface TimelineDequeueAction { const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string) => void) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const queuedCount = state.timelines.get(timelineId)?.totalQueuedItemsCount || 0; + const queuedCount = state.timelines[timelineId]?.totalQueuedItemsCount || 0; if (queuedCount <= 0) return; @@ -185,9 +185,9 @@ const fetchHomeTimeline = (expand = false, done = noOp) => const params: HomeTimelineParams = {}; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get('home')?.isLoading) return; + if (expand && state.timelines.home?.isLoading) return; - const fn = (expand && state.timelines.get('home')?.next?.()) || getClient(state).timelines.homeTimeline(params); + const fn = (expand && state.timelines.home?.next?.()) || getClient(state).timelines.homeTimeline(params); return dispatch(handleTimelineExpand('home', fn, false, done)); }; @@ -200,9 +200,9 @@ const fetchPublicTimeline = ({ onlyMedia, local, instance }: Record const params: PublicTimelineParams = { only_media: onlyMedia, local: instance ? false : local, instance }; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.publicTimeline(params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).timelines.publicTimeline(params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; @@ -215,9 +215,9 @@ const fetchBubbleTimeline = ({ onlyMedia }: Record = {}, expand = f const params: PublicTimelineParams = { only_media: onlyMedia }; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.bubbleTimeline(params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).timelines.bubbleTimeline(params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; @@ -231,9 +231,9 @@ const fetchAccountTimeline = (accountId: string, { exclude_replies, pinned, only if (pinned || only_media) params.with_muted = true; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).accounts.getAccountStatuses(accountId, params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).accounts.getAccountStatuses(accountId, params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; @@ -246,9 +246,9 @@ const fetchListTimeline = (listId: string, expand = false, done = noOp) => const params: ListTimelineParams = {}; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.listTimeline(listId, params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).timelines.listTimeline(listId, params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; @@ -262,9 +262,9 @@ const fetchGroupTimeline = (groupId: string, { only_media, limit }: Record = { none: parseTags(tags, 'none'), }; - if (expand && state.timelines.get(timelineId)?.isLoading) return; + if (expand && state.timelines[timelineId]?.isLoading) return; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.hashtagTimeline(hashtag, params); + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).timelines.hashtagTimeline(hashtag, params); return dispatch(handleTimelineExpand(timelineId, fn, false, done)); }; diff --git a/packages/pl-fe/src/features/account-gallery/index.tsx b/packages/pl-fe/src/features/account-gallery/index.tsx index 9a152e70e..6925548d9 100644 --- a/packages/pl-fe/src/features/account-gallery/index.tsx +++ b/packages/pl-fe/src/features/account-gallery/index.tsx @@ -27,8 +27,8 @@ const AccountGallery = () => { } = useAccountLookup(username, { withRelationship: true }); const attachments: Array = useAppSelector((state) => account ? getAccountGallery(state, account.id) : []); - const isLoading = useAppSelector((state) => state.timelines.get(`account:${account?.id}:with_replies:media`)?.isLoading); - const hasMore = useAppSelector((state) => state.timelines.get(`account:${account?.id}:with_replies:media`)?.hasMore); + const isLoading = useAppSelector((state) => state.timelines[`account:${account?.id}:with_replies:media`]?.isLoading); + const hasMore = useAppSelector((state) => state.timelines[`account:${account?.id}:with_replies:media`]?.hasMore); const handleScrollToBottom = () => { if (hasMore) { diff --git a/packages/pl-fe/src/features/account-timeline/index.tsx b/packages/pl-fe/src/features/account-timeline/index.tsx index 276d46db5..3f9bbf8cf 100644 --- a/packages/pl-fe/src/features/account-timeline/index.tsx +++ b/packages/pl-fe/src/features/account-timeline/index.tsx @@ -41,8 +41,8 @@ const AccountTimeline: React.FC = ({ params, withReplies = fal const isBlocked = account?.relationship?.blocked_by; const unavailable = isBlocked && !features.blockersVisible; - const isLoading = useAppSelector(state => state.timelines.get(`account:${path}`)?.isLoading === true); - const hasMore = useAppSelector(state => state.timelines.get(`account:${path}`)?.hasMore === true); + const isLoading = useAppSelector(state => state.timelines[`account:${path}`]?.isLoading === true); + const hasMore = useAppSelector(state => state.timelines[`account:${path}`]?.hasMore === true); const accountUsername = account?.username || params.username; diff --git a/packages/pl-fe/src/features/home-timeline/index.tsx b/packages/pl-fe/src/features/home-timeline/index.tsx index 232580046..cab35870d 100644 --- a/packages/pl-fe/src/features/home-timeline/index.tsx +++ b/packages/pl-fe/src/features/home-timeline/index.tsx @@ -29,7 +29,7 @@ const HomeTimeline: React.FC = () => { const polling = useRef(null); const isMobile = useIsMobile(); - const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true); + const isPartial = useAppSelector(state => state.timelines.home?.isPartial === true); const handleLoadMore = (maxId: string) => { dispatch(fetchHomeTimeline(true)); diff --git a/packages/pl-fe/src/features/ui/components/modals/report-modal/steps/other-actions-step.tsx b/packages/pl-fe/src/features/ui/components/modals/report-modal/steps/other-actions-step.tsx index 7cc3131c5..9a3571c17 100644 --- a/packages/pl-fe/src/features/ui/components/modals/report-modal/steps/other-actions-step.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/report-modal/steps/other-actions-step.tsx @@ -1,4 +1,3 @@ -import { OrderedSet } from 'immutable'; import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; @@ -47,7 +46,7 @@ const OtherActionsStep = ({ const features = useFeatures(); const intl = useIntl(); - const statusIds = useAppSelector((state) => OrderedSet(state.timelines.get(`account:${account.id}:with_replies`)!.items).union(selectedStatusIds) as OrderedSet); + const statusIds = useAppSelector((state) => [...new Set([...state.timelines[`account:${account.id}:with_replies`]!.items, ...selectedStatusIds])]); const isBlocked = block; const isForward = forward; const canForward = !account.local && features.federating; diff --git a/packages/pl-fe/src/features/ui/components/timeline.tsx b/packages/pl-fe/src/features/ui/components/timeline.tsx index e85d82ba2..68498deaf 100644 --- a/packages/pl-fe/src/features/ui/components/timeline.tsx +++ b/packages/pl-fe/src/features/ui/components/timeline.tsx @@ -35,10 +35,10 @@ const Timeline: React.FC = ({ const statusIds = useAppSelector(state => getStatusIds(state, { type: timelineId, prefix })); const lastStatusId = statusIds.at(-1); - const isLoading = useAppSelector(state => (state.timelines.get(timelineId) || { isLoading: true }).isLoading === true); - const isPartial = useAppSelector(state => (state.timelines.get(timelineId)?.isPartial || false) === true); - const hasMore = useAppSelector(state => state.timelines.get(timelineId)?.hasMore === true); - const totalQueuedItemsCount = useAppSelector(state => state.timelines.get(timelineId)?.totalQueuedItemsCount || 0); + const isLoading = useAppSelector(state => state.timelines[timelineId]?.isLoading !== false); + const isPartial = useAppSelector(state => (state.timelines[timelineId]?.isPartial || false) === true); + const hasMore = useAppSelector(state => state.timelines[timelineId]?.hasMore === true); + const totalQueuedItemsCount = useAppSelector(state => state.timelines[timelineId]?.totalQueuedItemsCount || 0); const handleDequeueTimeline = useCallback(() => { dispatch(dequeueTimeline(timelineId, onLoadMore)); diff --git a/packages/pl-fe/src/reducers/timelines.ts b/packages/pl-fe/src/reducers/timelines.ts index 763341c47..55d63b1c2 100644 --- a/packages/pl-fe/src/reducers/timelines.ts +++ b/packages/pl-fe/src/reducers/timelines.ts @@ -1,10 +1,5 @@ -import { - Map as ImmutableMap, - List as ImmutableList, - OrderedSet as ImmutableOrderedSet, - Record as ImmutableRecord, -} from 'immutable'; import sample from 'lodash/sample'; +import { create } from 'mutative'; import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from '../actions/accounts'; import { PIN_SUCCESS, UNPIN_SUCCESS, type InteractionsAction } from '../actions/interactions'; @@ -32,50 +27,68 @@ import type { AnyAction } from 'redux'; const TRUNCATE_LIMIT = 40; const TRUNCATE_SIZE = 20; -const TimelineRecord = ImmutableRecord({ +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; +} + +const newTimeline = (): Timeline => ({ unread: 0, top: true, isLoading: false, hasMore: true, - next: null as (() => Promise>) | null, - prev: null as (() => Promise>) | null, - items: ImmutableOrderedSet(), - queuedItems: ImmutableOrderedSet(), //max= MAX_QUEUED_ITEMS + next: null, + prev: null, + items: [], + queuedItems: [], //max= MAX_QUEUED_ITEMS totalQueuedItemsCount: 0, //used for queuedItems overflow for MAX_QUEUED_ITEMS+ loadingFailed: false, isPartial: false, }); -const initialState = ImmutableMap(); +const initialState: State = {}; -type State = ImmutableMap; -type Timeline = ReturnType; +type State = Record; -const getStatusIds = (statuses: Array> = []) => ( - ImmutableOrderedSet(statuses.map(status => status.id)) -); +const getStatusIds = (statuses: Array> = []) => statuses.map(status => status.id); -const mergeStatusIds = (oldIds = ImmutableOrderedSet(), newIds = ImmutableOrderedSet()) => ( - newIds.union(oldIds) -); +const mergeStatusIds = (oldIds: Array, newIds: Array) => [...new Set([...newIds, ...oldIds])]; -const addStatusId = (oldIds = ImmutableOrderedSet(), newId: string) => ( - mergeStatusIds(oldIds, ImmutableOrderedSet([newId])) +const addStatusId = (oldIds = Array(), newId: string) => ( + mergeStatusIds(oldIds, [newId]) ); // Like `take`, but only if the collection's size exceeds truncateLimit -const truncate = (items: ImmutableOrderedSet, truncateLimit: number, newSize: number) => ( - items.size > truncateLimit ? items.take(newSize) : items +const truncate = (items: Array, truncateLimit: number, newSize: number) => ( + items.length > truncateLimit ? items.slice(0, newSize) : items ); -const truncateIds = (items: ImmutableOrderedSet) => truncate(items, TRUNCATE_LIMIT, TRUNCATE_SIZE); +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) => - state.update(timelineId, TimelineRecord(), timeline => timeline.set('isLoading', loading)); + updateTimeline(state, timelineId, (timeline) => { + timeline.isLoading = loading; + }); // Keep track of when a timeline failed to load const setFailed = (state: State, timelineId: string, failed: boolean) => - state.update(timelineId, TimelineRecord(), timeline => timeline.set('loadingFailed', failed)); + updateTimeline(state, timelineId, (timeline) => { + timeline.loadingFailed = failed; + }); const expandNormalizedTimeline = ( state: State, @@ -89,65 +102,61 @@ const expandNormalizedTimeline = ( ) => { const newIds = getStatusIds(statuses); - return state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => { - timeline.set('isLoading', false); - timeline.set('loadingFailed', false); - timeline.set('isPartial', isPartial); - timeline.set('next', next); - timeline.set('prev', prev); + updateTimeline(state, timelineId, (timeline) => { + timeline.isLoading = false; + timeline.loadingFailed = false; + timeline.isPartial = isPartial; + timeline.next = next; + timeline.prev = prev; - if (!next && !isLoadingRecent) timeline.set('hasMore', false); + if (!next && !isLoadingRecent) timeline.hasMore = false; // Pinned timelines can be replaced entirely if (timelineId.endsWith(':pinned')) { - timeline.set('items', newIds); + timeline.items = newIds; return; } - if (!newIds.isEmpty()) { - timeline.update('items', oldIds => { - if (pos === 'end') { - return mergeStatusIds(newIds, oldIds); - } else { - return mergeStatusIds(oldIds, newIds); - } - }); + if (newIds.length) { + if (pos === 'end') { + timeline.items = mergeStatusIds(newIds, timeline.items); + } else { + timeline.items = mergeStatusIds(timeline.items, newIds); + } } - })); + }); }; -const updateTimeline = (state: State, timelineId: string, statusId: string) => { - const top = state.get(timelineId)?.top; - const oldIds = state.get(timelineId)?.items || ImmutableOrderedSet(); - const unread = state.get(timelineId)?.unread || 0; +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)) return state; const newIds = addStatusId(oldIds, statusId); - return state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => { + updateTimeline(state, timelineId, (timeline) => { if (top) { // For performance, truncate items if user is scrolled to the top - timeline.set('items', truncateIds(newIds)); + timeline.items = truncateIds(newIds); } else { - timeline.set('unread', unread + 1); - timeline.set('items', newIds); + timeline.unread = unread + 1; + timeline.items = newIds; } - })); + }); }; const updateTimelineQueue = (state: State, timelineId: string, statusId: string) => { - const queuedIds = state.get(timelineId)?.queuedItems || ImmutableOrderedSet(); - const listedIds = state.get(timelineId)?.items || ImmutableOrderedSet(); - const queuedCount = state.get(timelineId)?.totalQueuedItemsCount || 0; + updateTimeline(state, timelineId, (timeline) => { + const { queuedItems: queuedIds, items: listedIds, totalQueuedItemsCount: queuedCount } = timeline; - if (queuedIds.includes(statusId)) return state; - if (listedIds.includes(statusId)) return state; + if (queuedIds.includes(statusId)) return; + if (listedIds.includes(statusId)) return; - return state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => { - timeline.set('totalQueuedItemsCount', queuedCount + 1); - timeline.set('queuedItems', addStatusId(queuedIds, statusId).take(MAX_QUEUED_ITEMS)); - })); + timeline.totalQueuedItemsCount = queuedCount + 1; + timeline.queuedItems = addStatusId(queuedIds, statusId).slice(0, MAX_QUEUED_ITEMS); + }); }; const shouldDelete = (timelineId: string, excludeAccount?: string) => { @@ -157,39 +166,39 @@ const shouldDelete = (timelineId: string, excludeAccount?: string) => { return true; }; -const deleteStatus = (state: State, statusId: string, references: ImmutableMap | Array<[string, string]>, excludeAccount?: string) => - state.withMutations(state => { - state.keySeq().forEach(timelineId => { - if (shouldDelete(timelineId, excludeAccount)) { - state.updateIn([timelineId, 'items'], ids => (ids as ImmutableOrderedSet).delete(statusId)); - state.updateIn([timelineId, 'queuedItems'], ids => (ids as ImmutableOrderedSet).delete(statusId)); - } - }); +const deleteStatus = (state: State, statusId: string, references: Array, excludeAccount?: string) => { + 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); - }); + // Remove reblogs of deleted status + references.forEach(ref => { + deleteStatus(state, ref, [], excludeAccount); }); +}; -const clearTimeline = (state: State, timelineId: string) => state.set(timelineId, TimelineRecord()); +const clearTimeline = (state: State, timelineId: string) => { + state[timelineId] = newTimeline(); +}; const updateTop = (state: State, timelineId: string, top: boolean) => - state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => { - if (top) timeline.set('unread', 0); - timeline.set('top', top); - })); + updateTimeline(state, timelineId, (timeline) => { + if (top) timeline.unread = 0; + timeline.top = top; + }); const isReblogOf = (reblog: Pick, status: Pick) => reblog.reblog_id === status.id; -const statusToReference = (status: Pick) => [status.id, status.account]; const buildReferencesTo = ( - statuses: ImmutableMap>, + statuses: Record>, status: Pick, ) => ( - statuses + Object.values(statuses) .filter(reblog => isReblogOf(reblog, status)) - .map(statusToReference) as ImmutableMap + .map(status => status.id) ); // const filterTimeline = (state: State, timelineId: string, relationship: APIEntity, statuses: ImmutableList>) => @@ -198,29 +207,28 @@ const buildReferencesTo = ( // statuses.getIn([statusId, 'account']) === relationship.id, // )); -const filterTimelines = (state: State, relationship: Relationship, statuses: ImmutableMap>) => - state.withMutations(state => { - statuses.forEach(status => { - if (status.account_id !== relationship.id) return; - const references = buildReferencesTo(statuses, status); - deleteStatus(state, status.id, references, relationship.id); - }); - }); +const filterTimelines = (state: State, relationship: Relationship, statuses: Record>) => { + for (const statusId in statuses) { + const status = statuses[statusId]; + + if (status.account_id !== relationship.id) return; + const references = buildReferencesTo(statuses, status); + deleteStatus(state, status.id, references, relationship.id); + } +}; const timelineDequeue = (state: State, timelineId: string) => { - const top = state.getIn([timelineId, 'top']); + updateTimeline(state, timelineId, (timeline) => { + const top = timeline.top; - return state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations((timeline: Timeline) => { const queuedIds = timeline.queuedItems; - timeline.update('items', ids => { - const newIds = mergeStatusIds(ids, queuedIds); - return top ? truncateIds(newIds) : newIds; - }); + const newIds = mergeStatusIds(timeline.items, queuedIds); + timeline.items = top ? truncateIds(newIds) : newIds; - timeline.set('queuedItems', ImmutableOrderedSet()); - timeline.set('totalQueuedItemsCount', 0); - })); + timeline.queuedItems = []; + timeline.totalQueuedItemsCount = 0; + }); }; // const timelineDisconnect = (state: State, timelineId: string) => @@ -244,26 +252,21 @@ const getTimelinesForStatus = (status: Pick) }; // Given an OrderedSet of IDs, replace oldId with newId maintaining its position -const replaceId = (ids: ImmutableOrderedSet, oldId: string, newId: string) => { - const list = ImmutableList(ids); - const index = list.indexOf(oldId); +const replaceId = (ids: Array, oldId: string, newId: string) => { + const index = ids.indexOf(oldId); if (index > -1) { - return ImmutableOrderedSet(list.set(index, newId)); - } else { - return ids; + ids[index] = newId; } }; const importPendingStatus = (state: State, params: BaseStatus, idempotencyKey: string) => { const statusId = `末pending-${idempotencyKey}`; - return state.withMutations(state => { - const timelineIds = getTimelinesForStatus(params); + const timelineIds = getTimelinesForStatus(params); - timelineIds.forEach(timelineId => { - updateTimelineQueue(state, timelineId, statusId); - }); + timelineIds.forEach(timelineId => { + updateTimelineQueue(state, timelineId, statusId); }); }; @@ -271,96 +274,89 @@ const replacePendingStatus = (state: State, idempotencyKey: string, newId: strin const oldId = `末pending-${idempotencyKey}`; // Loop through timelines and replace the pending status with the real one - return state.withMutations(state => { - state.keySeq().forEach(timelineId => { - state.updateIn([timelineId, 'items'], ids => replaceId((ids as ImmutableOrderedSet), oldId, newId)); - state.updateIn([timelineId, 'queuedItems'], ids => replaceId((ids as ImmutableOrderedSet), oldId, newId)); - }); + for (const timelineId in state) { + replaceId(state[timelineId].items, oldId, newId); + replaceId(state[timelineId].queuedItems, oldId, newId); + } +}; + +const importStatus = (state: State, status: BaseStatus, idempotencyKey: string) =>{ + replacePendingStatus(state, idempotencyKey, status.id); + + const timelineIds = getTimelinesForStatus(status); + + timelineIds.forEach(timelineId => { + appendStatus(state, timelineId, status.id); }); }; -const importStatus = (state: State, status: BaseStatus, idempotencyKey: string) => - state.withMutations(state => { - replacePendingStatus(state, idempotencyKey, status.id); - - const timelineIds = getTimelinesForStatus(status); - - timelineIds.forEach(timelineId => { - updateTimeline(state, timelineId, status.id); - }); - }); - -const handleExpandFail = (state: State, timelineId: string) => state.withMutations(state => { +const handleExpandFail = (state: State, timelineId: string) => { setLoading(state, timelineId, false); setFailed(state, timelineId, true); -}); +}; -const timelines = (state: State = initialState, action: AnyAction | InteractionsAction | StatusesAction | TimelineAction) => { +const timelines = (state: State = initialState, action: AnyAction | InteractionsAction | StatusesAction | TimelineAction): State => { switch (action.type) { case STATUS_CREATE_REQUEST: if (action.params.scheduled_at) return state; - return importPendingStatus(state, action.params, action.idempotencyKey); + return create(state, (draft) => importPendingStatus(draft, action.params, action.idempotencyKey)); case STATUS_CREATE_SUCCESS: if (action.status.scheduled_at || action.editing) return state; - return importStatus(state, action.status, action.idempotencyKey); + return create(state, (draft) => importStatus(draft, action.status, action.idempotencyKey)); case TIMELINE_EXPAND_REQUEST: - return setLoading(state, action.timeline, true); + return create(state, (draft) => setLoading(draft, action.timeline, true)); case TIMELINE_EXPAND_FAIL: - return handleExpandFail(state, action.timeline); + return create(state, (draft) => handleExpandFail(draft, action.timeline)); case TIMELINE_EXPAND_SUCCESS: - return expandNormalizedTimeline( - state, + return create(state, (draft) => expandNormalizedTimeline( + draft, action.timeline, action.statuses, action.next, action.prev, action.partial, action.isLoadingRecent, - ); + )); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, action.statusId); + return create(state, (draft) => appendStatus(draft, action.timeline, action.statusId)); case TIMELINE_UPDATE_QUEUE: - return updateTimelineQueue(state, action.timeline, action.statusId); + return create(state, (draft) => updateTimelineQueue(draft, action.timeline, action.statusId)); case TIMELINE_DEQUEUE: - return timelineDequeue(state, action.timeline); + return create(state, (draft) => timelineDequeue(draft, action.timeline)); case TIMELINE_DELETE: - return deleteStatus(state, action.statusId, action.references, action.reblogOf); + return create(state, (draft) => deleteStatus(draft, action.statusId, action.references, action.reblogOf)); case TIMELINE_CLEAR: - return clearTimeline(state, action.timeline); + return create(state, (draft) => clearTimeline(draft, action.timeline)); case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_MUTE_SUCCESS: - return filterTimelines(state, action.relationship, action.statuses); + return create(state, (draft) => filterTimelines(draft, action.relationship, action.statuses)); // case ACCOUNT_UNFOLLOW_SUCCESS: // return filterTimeline(state, 'home', action.relationship, action.statuses); case TIMELINE_SCROLL_TOP: - return updateTop(state, action.timeline, action.top); + return create(state, (draft) => updateTop(state, action.timeline, action.top)); case TIMELINE_INSERT: - return state.update(action.timeline, TimelineRecord(), timeline => timeline.withMutations(timeline => { - timeline.update('items', oldIds => { + return create(state, (draft) => updateTimeline(draft, action.timeline, (timeline) => { + let oldIdsArray = timeline.items; + const existingSuggestionId = oldIdsArray.find(key => key.includes('末suggestions')); - let oldIdsArray = oldIds.toArray(); - const existingSuggestionId = oldIdsArray.find(key => key.includes('末suggestions')); + if (existingSuggestionId) { + oldIdsArray = oldIdsArray.slice(1); + } + const positionInTimeline = sample([5, 6, 7, 8, 9]) as number; + if (timeline.items.at(-1)) { + oldIdsArray.splice(positionInTimeline, 0, `末suggestions-${timeline.items.at(-1)}`); + } - if (existingSuggestionId) { - oldIdsArray = oldIdsArray.slice(1); - } - const positionInTimeline = sample([5, 6, 7, 8, 9]) as number; - if (oldIds.last()) { - oldIdsArray.splice(positionInTimeline, 0, `末suggestions-${oldIds.last()}`); - } - return ImmutableOrderedSet(oldIdsArray); - }); + timeline.items = oldIdsArray; })); case PIN_SUCCESS: - return state.updateIn( - [`account:${action.accountId}:with_replies:pinned`, 'items'], - ids => ImmutableOrderedSet([action.statusId]).union(ids as ImmutableOrderedSet), - ); + return create(state, (draft) => updateTimeline(draft, `account:${action.accountId}:with_replies:pinned`, (timeline) => { + timeline.items = [...new Set([action.statusId, ...timeline.items])]; + })); case UNPIN_SUCCESS: - return state.updateIn( - [`account:${action.accountId}:with_replies:pinned`, 'items'], - ids => (ids as ImmutableOrderedSet).delete(action.statusId), - ); + return create(state, (draft) => updateTimeline(draft, `account:${action.accountId}:with_replies:pinned`, (timeline) => { + timeline.items = timeline.items.filter((id) => id !== action.statusId); + })); default: return state; } diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index 1f757c1eb..40f3d5da5 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -209,7 +209,7 @@ type AccountGalleryAttachment = MediaAttachment & { } const getAccountGallery = createSelector([ - (state: RootState, id: string) => state.timelines.get(`account:${id}:with_replies:media`)?.items || ImmutableOrderedSet(), + (state: RootState, id: string) => state.timelines[`account:${id}:with_replies:media`]?.items || [], (state: RootState) => state.statuses, ], (statusIds, statuses) => statusIds.reduce((medias: Array, statusId: string) => { @@ -223,7 +223,7 @@ const getAccountGallery = createSelector([ ); const getGroupGallery = createSelector([ - (state: RootState, id: string) => state.timelines.get(`group:${id}:media`)?.items || ImmutableOrderedSet(), + (state: RootState, id: string) => state.timelines[`group:${id}:media`]?.items || [], (state: RootState) => state.statuses, ], (statusIds, statuses) => statusIds.reduce((medias: Array, statusId: string) => { @@ -340,14 +340,14 @@ type ColumnQuery = { type: string; prefix?: string }; const makeGetStatusIds = () => createSelector([ (state: RootState, { type, prefix }: ColumnQuery) => useSettingsStore.getState().settings.timelines[prefix || type], - (state: RootState, { type }: ColumnQuery) => state.timelines.get(type)?.items || ImmutableOrderedSet(), + (state: RootState, { type }: ColumnQuery) => state.timelines[type]?.items || [], (state: RootState) => state.statuses, -], (columnSettings: any, statusIds: ImmutableOrderedSet, statuses) => +], (columnSettings: any, statusIds: Array, statuses) => statusIds.filter((id: string) => { const status = statuses[id]; if (!status) return true; return !shouldFilter(status, columnSettings); - }).toArray(), + }), ); export {