Do not use immutable for timelines

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-10-29 13:32:12 +01:00
parent 533c83c1b2
commit 632905f591
12 changed files with 143 additions and 154 deletions

View file

@ -70,7 +70,7 @@ const fetchHomeTimeline = (expand = false, done = noOp) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const fn = (expand && state.timelines.get('home')?.next?.()) || client.timelines.homeTimeline();
const fn = (expand && state.timelines.home?.next?.()) || client.timelines.homeTimeline();
return dispatch(handleTimelineExpand('home', fn, false, done));
};
@ -82,7 +82,7 @@ const fetchPublicTimeline = ({ onlyMedia, local, instance }: Record<string, any>
const params: PublicTimelineParams = { only_media: onlyMedia, local: instance ? false : local, instance };
const fn = (expand && state.timelines.get(timelineId)?.next?.()) || client.timelines.publicTimeline(params);
const fn = (expand && state.timelines[timelineId]?.next?.()) || client.timelines.publicTimeline(params);
return dispatch(handleTimelineExpand(timelineId, fn, false, done));
};
@ -95,7 +95,7 @@ const fetchAccountTimeline = (accountId: string, { exclude_replies, pinned, only
const params: GetAccountStatusesParams = { exclude_replies, pinned, only_media, limit, tagged };
if (pinned || only_media) params.with_muted = true;
const fn = (expand && state.timelines.get(timelineId)?.next?.()) || client.accounts.getAccountStatuses(accountId, params);
const fn = (expand && state.timelines[timelineId]?.next?.()) || client.accounts.getAccountStatuses(accountId, params);
return dispatch(handleTimelineExpand(timelineId, fn, false, done));
};
@ -108,7 +108,7 @@ const fetchGroupTimeline = (groupId: string, { only_media, limit }: Record<strin
const params: GroupTimelineParams = { only_media, limit };
if (only_media) params.with_muted = true;
const fn = (expand && state.timelines.get(timelineId)?.next?.()) || client.timelines.groupTimeline(groupId, params);
const fn = (expand && state.timelines[timelineId]?.next?.()) || client.timelines.groupTimeline(groupId, params);
return dispatch(handleTimelineExpand(timelineId, fn, false, done));
};
@ -118,7 +118,7 @@ const fetchHashtagTimeline = (hashtag: string, { tags }: Record<string, any> = {
const state = getState();
const timelineId = `hashtag:${hashtag}`;
const fn = (expand && state.timelines.get(timelineId)?.next?.()) || client.timelines.hashtagTimeline(hashtag);
const fn = (expand && state.timelines[timelineId]?.next?.()) || client.timelines.hashtagTimeline(hashtag);
return dispatch(handleTimelineExpand(timelineId, fn, false, done));
};

View file

@ -8,15 +8,14 @@ import StatusContainer from 'bigbuffet/containers/status-container';
import PlaceholderStatus from 'bigbuffet/features/placeholder/components/placeholder-status';
import type { IScrollableList } from 'bigbuffet/components/scrollable-list';
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
/** List of status IDs to display. */
statusIds: ImmutableOrderedSet<string>;
statusIds: Array<string>;
/** Last _unfiltered_ status ID (maxId) for pagination. */
lastStatusId?: string;
/** Pinned statuses to show at the top of the feed. */
featuredStatusIds?: ImmutableOrderedSet<string>;
featuredStatusIds?: Array<string>;
/** Pagination callback when the end of the list is reached. */
onLoadMore?: (lastStatusId: string) => void;
/** Whether the data is currently being fetched. */
@ -38,13 +37,13 @@ const StatusList: React.FC<IStatusList> = ({
isLoading,
...other
}) => {
const getFeaturedStatusCount = () => featuredStatusIds?.size || 0;
const getFeaturedStatusCount = () => featuredStatusIds?.length || 0;
const getCurrentStatusIndex = (id: string, featured: boolean): number => {
if (featured) {
return featuredStatusIds?.keySeq().findIndex(key => key === id) || 0;
return featuredStatusIds?.findIndex(key => key === id) || 0;
} else {
return statusIds.keySeq().findIndex(key => key === id) + getFeaturedStatusCount();
return statusIds.findIndex(key => key === id) + getFeaturedStatusCount();
}
};
@ -59,11 +58,11 @@ const StatusList: React.FC<IStatusList> = ({
};
const handleLoadOlder = useCallback(debounce(() => {
const maxId = lastStatusId || statusIds.last();
const maxId = statusIds.at(-1);
if (onLoadMore && maxId) {
onLoadMore(maxId.replace('末suggestions-', ''));
}
}, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds.last()]);
}, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds]);
const selectChild = (index: number) => {
const selector = `#status-list [data-index="${index}"] .focusable`;
@ -73,9 +72,9 @@ const StatusList: React.FC<IStatusList> = ({
};
const renderLoadGap = (index: number) => {
const ids = statusIds.toList();
const nextId = ids.get(index + 1);
const prevId = ids.get(index - 1);
const ids = statusIds;
const nextId = ids[index + 1];
const prevId = ids[index - 1];
if (index < 1 || !nextId || !prevId || !onLoadMore) return null;
@ -101,7 +100,7 @@ const StatusList: React.FC<IStatusList> = ({
const renderFeaturedStatuses = (): React.ReactNode[] => {
if (!featuredStatusIds) return [];
return featuredStatusIds.toArray().map(statusId => (
return featuredStatusIds.map(statusId => (
<StatusContainer
key={`f-${statusId}`}
id={statusId}
@ -113,8 +112,8 @@ const StatusList: React.FC<IStatusList> = ({
};
const renderStatuses = (): React.ReactNode[] => {
if (isLoading || statusIds.size > 0) {
return statusIds.toList().reduce((acc, statusId, index) => {
if (isLoading || statusIds.length > 0) {
return statusIds.reduce((acc, statusId, index) => {
if (statusId === null) {
const gap = renderLoadGap(index);
if (gap) {
@ -147,7 +146,7 @@ const StatusList: React.FC<IStatusList> = ({
id='status-list'
key='scrollable-list'
isLoading={isLoading}
showLoading={isLoading && statusIds.size === 0}
showLoading={isLoading && statusIds.length === 0}
onLoadMore={handleLoadOlder}
placeholderComponent={PlaceholderStatus}
placeholderCount={20}

View file

@ -39,11 +39,11 @@ const LoadMoreMedia: React.FC<ILoadMoreMedia> = ({ maxId, onLoadMore }) => {
};
const useAccountGallery = (accountId?: string) => {
const statusIds = useAppSelector((state) => state.timelines.get(`account:${accountId}:with_replies:media`)?.items);
const statusIds = useAppSelector((state) => state.timelines[`account:${accountId}:with_replies:media`]?.items);
const queryClient = usePlHooksQueryClient();
return useQueries<UseQueryOptions<NormalizedStatus>[]>({
queries: (statusIds?.toArray() || []).map(statusId => ({
queries: (statusIds || []).map(statusId => ({
queryKey: ['statuses', 'entities', statusId],
})),
}, queryClient).reduce<Array<MediaAttachment & {
@ -69,8 +69,8 @@ const AccountGallery = () => {
} = useAccountLookup(username);
const attachments = useAccountGallery(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 node = useRef<HTMLDivElement>(null);

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import { useAccountLookup } from 'pl-hooks';
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
@ -17,8 +16,6 @@ interface IHashtagTimeline {
};
}
const emptyTimeline = ImmutableOrderedSet<string>();
export const AccountHashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
const id = params?.id || '';
@ -28,10 +25,10 @@ export const AccountHashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) =
const path = `account:${account?.id}:hashtag:${id}`;
const statusIds = useAppSelector(state => state.timelines.get(path)?.items) || emptyTimeline;
const statusIds = useAppSelector(state => state.timelines[path]?.items) || [];
const isLoading = useAppSelector(state => state.timelines.get(path)?.isLoading === true);
const hasMore = useAppSelector(state => state.timelines.get(path)?.hasMore === true);
const isLoading = useAppSelector(state => state.timelines[path]?.isLoading === true);
const hasMore = useAppSelector(state => state.timelines[path]?.hasMore === true);
useEffect(() => {
if (account) {

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import { useAccountLookup } from 'pl-hooks';
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
@ -24,11 +23,11 @@ const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = fal
const path = withReplies ? `${account?.id}:with_replies` : account?.id;
const showPins = !withReplies;
const statusIds = useAppSelector(state => state.timelines.get(`account:${path}`)?.items || ImmutableOrderedSet<string>());
const featuredStatusIds = useAppSelector(state => state.timelines.get(`account:${account?.id}:pinned`)?.items || ImmutableOrderedSet<string>());
const statusIds = useAppSelector(state => state.timelines[`account:${path}`]?.items || []);
const featuredStatusIds = useAppSelector(state => state.timelines[`account:${account?.id}:pinned`]?.items || []);
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);
useEffect(() => {
if (account && !withReplies) {

View file

@ -1,4 +1,3 @@
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
import { useStatus } from 'pl-hooks';
import React, { useEffect, useRef, useState } from 'react';
import { FormattedMessage } from 'react-intl';
@ -30,15 +29,15 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
const { data: status } = useStatus(props.params.statusId);
const descendantsIds = useAppSelector(state => {
let descendantsIds = ImmutableOrderedSet<string>();
let descendantsIds = new Set<string>();
if (status) {
const statusId = status.id;
descendantsIds = getDescendantsIds(state, statusId);
descendantsIds = descendantsIds.delete(statusId);
descendantsIds.delete(statusId);
}
return descendantsIds;
return Array.from(descendantsIds);
});
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
@ -60,12 +59,12 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
}, [props.params.statusId]);
const handleMoveUp = (id: string) => {
const index = ImmutableList(descendantsIds).indexOf(id);
const index = descendantsIds.indexOf(id);
_selectChild(index - 1);
};
const handleMoveDown = (id: string) => {
const index = ImmutableList(descendantsIds).indexOf(id);
const index = descendantsIds.indexOf(id);
_selectChild(index + 1);
};
@ -97,7 +96,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
/>
);
const renderChildren = (list: ImmutableOrderedSet<string>) => list.map(id => {
const renderChildren = (list: Array<string>) => list.map(id => {
if (id.endsWith('-tombstone')) {
return renderTombstone(id);
} else {
@ -105,7 +104,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
}
});
const hasDescendants = descendantsIds.size > 0;
const hasDescendants = descendantsIds.length > 0;
if (!status && isLoaded) {
return (
@ -120,7 +119,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
const children: JSX.Element[] = [];
if (hasDescendants) {
children.push(...renderChildren(descendantsIds).toArray());
children.push(...renderChildren(descendantsIds));
}
return (

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import { useStatusQuotes } from 'pl-hooks';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@ -15,9 +14,7 @@ const Quotes: React.FC = () => {
const intl = useIntl();
const { statusId } = useParams<{ statusId: string }>();
const { data, isLoading, hasNextPage, fetchNextPage } = useStatusQuotes(statusId);
const statusIds = ImmutableOrderedSet(data);
const { data: statusIds = [], isLoading, hasNextPage, fetchNextPage } = useStatusQuotes(statusId);
const emptyMessage = <FormattedMessage id='empty_column.quotes' defaultMessage='This post has not been quoted yet.' />;

View file

@ -1,6 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import clsx from 'clsx';
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useEffect, useRef } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
@ -26,11 +25,11 @@ export const getAncestorsIds = createSelector([
(_: RootState, statusId: string | undefined) => statusId,
(state: RootState) => state.contexts.inReplyTos,
], (statusId, inReplyTos) => {
let ancestorsIds = ImmutableOrderedSet<string>();
let ancestorsIds = new Set<string>();
let id: string | undefined = statusId;
while (id && !ancestorsIds.includes(id)) {
ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds);
while (id && !ancestorsIds.has(id)) {
ancestorsIds = new Set([id]).union(ancestorsIds);
id = inReplyTos.get(id);
}
@ -41,7 +40,7 @@ export const getDescendantsIds = createSelector([
(_: RootState, statusId: string) => statusId,
(state: RootState) => state.contexts.replies,
], (statusId, contextReplies) => {
let descendantsIds = ImmutableOrderedSet<string>();
let descendantsIds = new Set<string>();
const ids = [statusId];
while (ids.length > 0) {
@ -50,12 +49,12 @@ export const getDescendantsIds = createSelector([
const replies = contextReplies.get(id);
if (descendantsIds.includes(id)) {
if (descendantsIds.has(id)) {
break;
}
if (statusId !== id) {
descendantsIds = descendantsIds.union([id]);
descendantsIds = descendantsIds.union(new Set(id));
}
if (replies) {
@ -91,26 +90,29 @@ const Thread: React.FC<IThread> = ({
const showMedia = statusesMeta[status.id]?.visible ?? !status.sensitive;
const { ancestorsIds, descendantsIds } = useAppSelector((state) => {
let ancestorsIds = ImmutableOrderedSet<string>();
let descendantsIds = ImmutableOrderedSet<string>();
let ancestorsIds = new Set<string>();
let descendantsIds = new Set<string>();
if (status) {
const statusId = status.id;
ancestorsIds = getAncestorsIds(state, state.contexts.inReplyTos.get(statusId));
descendantsIds = getDescendantsIds(state, statusId);
ancestorsIds = ancestorsIds.delete(statusId).subtract(descendantsIds);
descendantsIds = descendantsIds.delete(statusId).subtract(ancestorsIds);
ancestorsIds.delete(statusId);
descendantsIds.forEach(ancestorsIds.delete);
descendantsIds.delete(statusId);
ancestorsIds.forEach(descendantsIds.delete);
}
return {
status,
ancestorsIds,
descendantsIds,
ancestorsIds: Array.from(ancestorsIds),
descendantsIds: Array.from(descendantsIds),
};
});
let initialTopMostItemIndex = ancestorsIds.size;
if (!useWindowScroll && initialTopMostItemIndex !== 0) initialTopMostItemIndex = ancestorsIds.size + 1;
let initialTopMostItemIndex = ancestorsIds.length;
if (!useWindowScroll && initialTopMostItemIndex !== 0) initialTopMostItemIndex = ancestorsIds.length + 1;
const node = useRef<HTMLDivElement>(null);
const statusRef = useRef<HTMLDivElement>(null);
@ -161,13 +163,13 @@ const Thread: React.FC<IThread> = ({
const handleMoveUp = (id: string) => {
if (id === status?.id) {
_selectChild(ancestorsIds.size - 1);
_selectChild(ancestorsIds.length - 1);
} else {
let index = ImmutableList(ancestorsIds).indexOf(id);
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = ImmutableList(descendantsIds).indexOf(id);
_selectChild(ancestorsIds.size + index);
index = descendantsIds.indexOf(id);
_selectChild(ancestorsIds.length + index);
} else {
_selectChild(index - 1);
}
@ -176,13 +178,13 @@ const Thread: React.FC<IThread> = ({
const handleMoveDown = (id: string) => {
if (id === status?.id) {
_selectChild(ancestorsIds.size + 1);
_selectChild(ancestorsIds.length + 1);
} else {
let index = ImmutableList(ancestorsIds).indexOf(id);
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = ImmutableList(descendantsIds).indexOf(id);
_selectChild(ancestorsIds.size + index + 2);
index = descendantsIds.indexOf(id);
_selectChild(ancestorsIds.length + index + 2);
} else {
_selectChild(index + 1);
}
@ -225,7 +227,7 @@ const Thread: React.FC<IThread> = ({
/>
);
const renderChildren = (list: ImmutableOrderedSet<string>) => list.map(id => {
const renderChildren = (list: Array<string>) => list.map(id => {
if (id.endsWith('-tombstone')) {
return renderTombstone(id);
} else {
@ -235,15 +237,15 @@ const Thread: React.FC<IThread> = ({
// Scroll focused status into view when thread updates.
useEffect(() => {
virtualizer.current?.scrollToIndex(ancestorsIds.size);
}, [status.id, ancestorsIds.size]);
virtualizer.current?.scrollToIndex(ancestorsIds.length);
}, [status.id, ancestorsIds.length]);
const handleOpenCompareHistoryModal = (status: Pick<Status, 'id'>) => {
openModal('COMPARE_HISTORY', { statusId: status.id });
};
const hasAncestors = ancestorsIds.size > 0;
const hasDescendants = descendantsIds.size > 0;
const hasAncestors = ancestorsIds.length > 0;
const hasDescendants = descendantsIds.length > 0;
type HotkeyHandlers = { [key: string]: (keyEvent?: KeyboardEvent) => void };
@ -291,7 +293,7 @@ const Thread: React.FC<IThread> = ({
}
if (hasAncestors) {
children.push(...renderChildren(ancestorsIds).toArray());
children.push(...renderChildren(ancestorsIds));
}
children.push(focusedStatus);
@ -302,11 +304,11 @@ const Thread: React.FC<IThread> = ({
<FormattedMessage id='status.replies' defaultMessage='Replies' />
</p>,
);
children.push(...renderChildren(descendantsIds).toArray());
children.push(...renderChildren(descendantsIds));
}
if (hasDescendants) {
children.push(...renderChildren(descendantsIds).toArray());
children.push(...renderChildren(descendantsIds));
}
return (

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import { useStatus } from 'pl-hooks';
import React, { useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -35,17 +34,18 @@ const StatusDetails: React.FC<IStatusDetails> = (props) => {
const isLoaded = statusQuery.isSuccess;
const ancestorsIds = useAppSelector(state => {
let ancestorsIds = ImmutableOrderedSet<string>();
let descendantsIds = ImmutableOrderedSet<string>();
let ancestorsIds = new Set<string>();
let descendantsIds = new Set<string>();
if (status) {
const statusId = status.id;
ancestorsIds = getAncestorsIds(state, state.contexts.inReplyTos.get(statusId));
descendantsIds = getDescendantsIds(state, statusId);
ancestorsIds = ancestorsIds.delete(statusId).subtract(descendantsIds);
ancestorsIds.delete(statusId);
descendantsIds.forEach(ancestorsIds.delete);
}
return ancestorsIds;
return Array.from(ancestorsIds);
});
/** Fetch the status (and context) from the API. */
@ -60,9 +60,9 @@ const StatusDetails: React.FC<IStatusDetails> = (props) => {
fetchData();
}, [props.params.statusId]);
if (ancestorsIds.size) {
if (ancestorsIds.length) {
return (
<Redirect to={`/@${status?.account.acct || '@'}/posts/${ancestorsIds.first()}`} />
<Redirect to={`/@${status?.account.acct || '@'}/posts/${ancestorsIds[0]}`} />
);
}

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React from 'react';
import StatusList, { IStatusList } from 'bigbuffet/components/status-list';
@ -15,15 +14,13 @@ const Timeline: React.FC<ITimeline> = ({
onLoadMore,
...rest
}) => {
const statusIds = useAppSelector(state => state.timelines.get(timelineId)?.items || ImmutableOrderedSet<string>());
const lastStatusId = statusIds.last();
const isLoading = useAppSelector(state => (state.timelines.get(timelineId) || { isLoading: true }).isLoading === true);
const hasMore = useAppSelector(state => state.timelines.get(timelineId)?.hasMore === true);
const statusIds = useAppSelector(state => state.timelines[timelineId]?.items || []);
const isLoading = useAppSelector(state => (state.timelines[timelineId] || { isLoading: true }).isLoading === true);
const hasMore = useAppSelector(state => state.timelines[timelineId]?.hasMore === true);
return (
<StatusList
timelineId={timelineId}
lastStatusId={lastStatusId}
statusIds={statusIds}
isLoading={isLoading}
hasMore={hasMore}

View file

@ -21,7 +21,7 @@ type ContextStatus = {
}
/** Import a single status into the reducer, setting replies and replyTos. */
const importStatus = (state: State, status: ContextStatus, idempotencyKey?: string): State => {
const importStatus = (state: State, status: ContextStatus): State => {
const { id, in_reply_to_id: inReplyToId } = status;
if (!inReplyToId) return state;

View file

@ -1,8 +1,4 @@
import {
Map as ImmutableMap,
OrderedSet as ImmutableOrderedSet,
Record as ImmutableRecord,
} from 'immutable';
import { produce } from 'immer';
import {
TIMELINE_CLEAR,
@ -14,36 +10,38 @@ import {
import type { PaginatedResponse, Status as BaseStatus } from 'pl-api';
const TimelineRecord = ImmutableRecord({
interface Timeline {
isLoading: boolean;
hasMore: boolean;
next: (() => Promise<PaginatedResponse<BaseStatus>>) | null;
prev: (() => Promise<PaginatedResponse<BaseStatus>>) | null;
items: Array<string>;
}
const emptyTimeline: Timeline = {
isLoading: false,
hasMore: true,
next: null as (() => Promise<PaginatedResponse<BaseStatus>>) | null,
prev: null as (() => Promise<PaginatedResponse<BaseStatus>>) | null,
items: ImmutableOrderedSet<string>(),
queuedItems: ImmutableOrderedSet<string>(), //max= MAX_QUEUED_ITEMS
loadingFailed: false,
isPartial: false,
});
next: null,
prev: null,
items: [],
};
const initialState = ImmutableMap<string, Timeline>();
const initialState: State = {};
type State = ImmutableMap<string, Timeline>;
type Timeline = ReturnType<typeof TimelineRecord>;
type State = Record<string, Timeline>;
const getStatusIds = (statuses: Array<Pick<BaseStatus, 'id'>> = []) => (
ImmutableOrderedSet(statuses.map(status => status.id))
Array.from(new Set(statuses.map(status => status.id)))
);
const mergeStatusIds = (oldIds = ImmutableOrderedSet<string>(), newIds = ImmutableOrderedSet<string>()) => (
newIds.union(oldIds)
const mergeStatusIds = (oldIds = new Array<string>(), newIds = new Array<string>()) => (
Array.from(new Set(newIds).union(new Set(oldIds)))
);
const setLoading = (state: State, timelineId: string, loading: boolean) =>
state.update(timelineId, TimelineRecord(), timeline => timeline.set('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));
const setLoading = (state: State, timelineId: string, loading: boolean) => {
if (!state[timelineId]) state[timelineId] = emptyTimeline;
state[timelineId].isLoading = loading;
};
const expandNormalizedTimeline = (
state: State,
@ -51,37 +49,37 @@ const expandNormalizedTimeline = (
statuses: Array<BaseStatus>,
next: (() => Promise<PaginatedResponse<BaseStatus>>) | null,
prev: (() => Promise<PaginatedResponse<BaseStatus>>) | null,
isPartial: boolean,
isLoadingRecent: boolean,
pos: 'start' | 'end' = 'end',
) => {
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);
if (!state[timelineId]) state[timelineId] = emptyTimeline;
if (!next && !isLoadingRecent) timeline.set('hasMore', false);
const timeline = state[timelineId];
// Pinned timelines can be replaced entirely
if (timelineId.endsWith(':pinned')) {
timeline.set('items', newIds);
return;
timeline.isLoading = false;
timeline.next = next;
timeline.prev = prev;
if (!next && !isLoadingRecent) timeline.hasMore = false;
// Pinned timelines can be replaced entirely
if (timelineId.endsWith(':pinned')) {
timeline.items = newIds;
return;
}
if (newIds.length) {
let newItems: Array<string>;
if (pos === 'end') {
newItems = mergeStatusIds(newIds, timeline.items);
} else {
newItems = mergeStatusIds(timeline.items, newIds);
}
if (!newIds.isEmpty()) {
timeline.update('items', oldIds => {
if (pos === 'end') {
return mergeStatusIds(newIds, oldIds);
} else {
return mergeStatusIds(oldIds, newIds);
}
});
}
}));
timeline.items = newItems;
}
};
// const shouldDelete = (timelineId: string, excludeAccount?: string | null) => {
@ -106,35 +104,36 @@ const expandNormalizedTimeline = (
// });
// });
const clearTimeline = (state: State, timelineId: string) => state.set(timelineId, TimelineRecord());
const clearTimeline = (state: State, timelineId: string) => {
state[timelineId] = emptyTimeline;
return state;
};
const handleExpandFail = (state: State, timelineId: string) =>
state.withMutations(state => {
setLoading(state, timelineId, false);
setFailed(state, timelineId, true);
});
const handleExpandFail = (state: State, timelineId: string) => {
setLoading(state, timelineId, false);
return state;
};
const timelines = (state: State = initialState, action: TimelineAction) => {
const timelines = (state: State = initialState, action: TimelineAction) => produce(state, (draft) => {
switch (action.type) {
case TIMELINE_EXPAND_REQUEST:
return setLoading(state, action.timeline, true);
return setLoading(draft, action.timeline, true);
case TIMELINE_EXPAND_FAIL:
return handleExpandFail(state, action.timeline);
return handleExpandFail(draft, action.timeline);
case TIMELINE_EXPAND_SUCCESS:
return expandNormalizedTimeline(
state,
draft,
action.timeline,
action.statuses,
action.next,
action.prev,
action.partial,
action.isLoadingRecent,
);
case TIMELINE_CLEAR:
return clearTimeline(state, action.timeline);
return clearTimeline(draft, action.timeline);
default:
return state;
return draft;
}
};
});
export default timelines;