Do not use immutable for timelines
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
533c83c1b2
commit
632905f591
12 changed files with 143 additions and 154 deletions
|
@ -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));
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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.' />;
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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]}`} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue