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) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
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));
|
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 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));
|
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 };
|
const params: GetAccountStatusesParams = { exclude_replies, pinned, only_media, limit, tagged };
|
||||||
if (pinned || only_media) params.with_muted = true;
|
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));
|
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 };
|
const params: GroupTimelineParams = { only_media, limit };
|
||||||
if (only_media) params.with_muted = true;
|
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));
|
return dispatch(handleTimelineExpand(timelineId, fn, false, done));
|
||||||
};
|
};
|
||||||
|
@ -118,7 +118,7 @@ const fetchHashtagTimeline = (hashtag: string, { tags }: Record<string, any> = {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const timelineId = `hashtag:${hashtag}`;
|
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));
|
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 PlaceholderStatus from 'bigbuffet/features/placeholder/components/placeholder-status';
|
||||||
|
|
||||||
import type { IScrollableList } from 'bigbuffet/components/scrollable-list';
|
import type { IScrollableList } from 'bigbuffet/components/scrollable-list';
|
||||||
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
|
|
||||||
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
||||||
/** List of status IDs to display. */
|
/** List of status IDs to display. */
|
||||||
statusIds: ImmutableOrderedSet<string>;
|
statusIds: Array<string>;
|
||||||
/** Last _unfiltered_ status ID (maxId) for pagination. */
|
/** Last _unfiltered_ status ID (maxId) for pagination. */
|
||||||
lastStatusId?: string;
|
lastStatusId?: string;
|
||||||
/** Pinned statuses to show at the top of the feed. */
|
/** 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. */
|
/** Pagination callback when the end of the list is reached. */
|
||||||
onLoadMore?: (lastStatusId: string) => void;
|
onLoadMore?: (lastStatusId: string) => void;
|
||||||
/** Whether the data is currently being fetched. */
|
/** Whether the data is currently being fetched. */
|
||||||
|
@ -38,13 +37,13 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
...other
|
...other
|
||||||
}) => {
|
}) => {
|
||||||
const getFeaturedStatusCount = () => featuredStatusIds?.size || 0;
|
const getFeaturedStatusCount = () => featuredStatusIds?.length || 0;
|
||||||
|
|
||||||
const getCurrentStatusIndex = (id: string, featured: boolean): number => {
|
const getCurrentStatusIndex = (id: string, featured: boolean): number => {
|
||||||
if (featured) {
|
if (featured) {
|
||||||
return featuredStatusIds?.keySeq().findIndex(key => key === id) || 0;
|
return featuredStatusIds?.findIndex(key => key === id) || 0;
|
||||||
} else {
|
} 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 handleLoadOlder = useCallback(debounce(() => {
|
||||||
const maxId = lastStatusId || statusIds.last();
|
const maxId = statusIds.at(-1);
|
||||||
if (onLoadMore && maxId) {
|
if (onLoadMore && maxId) {
|
||||||
onLoadMore(maxId.replace('末suggestions-', ''));
|
onLoadMore(maxId.replace('末suggestions-', ''));
|
||||||
}
|
}
|
||||||
}, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds.last()]);
|
}, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds]);
|
||||||
|
|
||||||
const selectChild = (index: number) => {
|
const selectChild = (index: number) => {
|
||||||
const selector = `#status-list [data-index="${index}"] .focusable`;
|
const selector = `#status-list [data-index="${index}"] .focusable`;
|
||||||
|
@ -73,9 +72,9 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLoadGap = (index: number) => {
|
const renderLoadGap = (index: number) => {
|
||||||
const ids = statusIds.toList();
|
const ids = statusIds;
|
||||||
const nextId = ids.get(index + 1);
|
const nextId = ids[index + 1];
|
||||||
const prevId = ids.get(index - 1);
|
const prevId = ids[index - 1];
|
||||||
|
|
||||||
if (index < 1 || !nextId || !prevId || !onLoadMore) return null;
|
if (index < 1 || !nextId || !prevId || !onLoadMore) return null;
|
||||||
|
|
||||||
|
@ -101,7 +100,7 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
const renderFeaturedStatuses = (): React.ReactNode[] => {
|
const renderFeaturedStatuses = (): React.ReactNode[] => {
|
||||||
if (!featuredStatusIds) return [];
|
if (!featuredStatusIds) return [];
|
||||||
|
|
||||||
return featuredStatusIds.toArray().map(statusId => (
|
return featuredStatusIds.map(statusId => (
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
key={`f-${statusId}`}
|
key={`f-${statusId}`}
|
||||||
id={statusId}
|
id={statusId}
|
||||||
|
@ -113,8 +112,8 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderStatuses = (): React.ReactNode[] => {
|
const renderStatuses = (): React.ReactNode[] => {
|
||||||
if (isLoading || statusIds.size > 0) {
|
if (isLoading || statusIds.length > 0) {
|
||||||
return statusIds.toList().reduce((acc, statusId, index) => {
|
return statusIds.reduce((acc, statusId, index) => {
|
||||||
if (statusId === null) {
|
if (statusId === null) {
|
||||||
const gap = renderLoadGap(index);
|
const gap = renderLoadGap(index);
|
||||||
if (gap) {
|
if (gap) {
|
||||||
|
@ -147,7 +146,7 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
id='status-list'
|
id='status-list'
|
||||||
key='scrollable-list'
|
key='scrollable-list'
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showLoading={isLoading && statusIds.size === 0}
|
showLoading={isLoading && statusIds.length === 0}
|
||||||
onLoadMore={handleLoadOlder}
|
onLoadMore={handleLoadOlder}
|
||||||
placeholderComponent={PlaceholderStatus}
|
placeholderComponent={PlaceholderStatus}
|
||||||
placeholderCount={20}
|
placeholderCount={20}
|
||||||
|
|
|
@ -39,11 +39,11 @@ const LoadMoreMedia: React.FC<ILoadMoreMedia> = ({ maxId, onLoadMore }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const useAccountGallery = (accountId?: string) => {
|
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();
|
const queryClient = usePlHooksQueryClient();
|
||||||
|
|
||||||
return useQueries<UseQueryOptions<NormalizedStatus>[]>({
|
return useQueries<UseQueryOptions<NormalizedStatus>[]>({
|
||||||
queries: (statusIds?.toArray() || []).map(statusId => ({
|
queries: (statusIds || []).map(statusId => ({
|
||||||
queryKey: ['statuses', 'entities', statusId],
|
queryKey: ['statuses', 'entities', statusId],
|
||||||
})),
|
})),
|
||||||
}, queryClient).reduce<Array<MediaAttachment & {
|
}, queryClient).reduce<Array<MediaAttachment & {
|
||||||
|
@ -69,8 +69,8 @@ const AccountGallery = () => {
|
||||||
} = useAccountLookup(username);
|
} = useAccountLookup(username);
|
||||||
|
|
||||||
const attachments = useAccountGallery(account?.id);
|
const attachments = useAccountGallery(account?.id);
|
||||||
const isLoading = useAppSelector((state) => state.timelines.get(`account:${account?.id}:with_replies:media`)?.isLoading);
|
const isLoading = useAppSelector((state) => state.timelines[`account:${account?.id}:with_replies:media`]?.isLoading);
|
||||||
const hasMore = useAppSelector((state) => state.timelines.get(`account:${account?.id}:with_replies:media`)?.hasMore);
|
const hasMore = useAppSelector((state) => state.timelines[`account:${account?.id}:with_replies:media`]?.hasMore);
|
||||||
|
|
||||||
const node = useRef<HTMLDivElement>(null);
|
const node = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import { useAccountLookup } from 'pl-hooks';
|
import { useAccountLookup } from 'pl-hooks';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
@ -17,8 +16,6 @@ interface IHashtagTimeline {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyTimeline = ImmutableOrderedSet<string>();
|
|
||||||
|
|
||||||
export const AccountHashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
export const AccountHashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
const id = params?.id || '';
|
const id = params?.id || '';
|
||||||
|
|
||||||
|
@ -28,10 +25,10 @@ export const AccountHashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) =
|
||||||
|
|
||||||
const path = `account:${account?.id}:hashtag:${id}`;
|
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 isLoading = useAppSelector(state => state.timelines[path]?.isLoading === true);
|
||||||
const hasMore = useAppSelector(state => state.timelines.get(path)?.hasMore === true);
|
const hasMore = useAppSelector(state => state.timelines[path]?.hasMore === true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (account) {
|
if (account) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import { useAccountLookup } from 'pl-hooks';
|
import { useAccountLookup } from 'pl-hooks';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
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 path = withReplies ? `${account?.id}:with_replies` : account?.id;
|
||||||
const showPins = !withReplies;
|
const showPins = !withReplies;
|
||||||
const statusIds = useAppSelector(state => state.timelines.get(`account:${path}`)?.items || ImmutableOrderedSet<string>());
|
const statusIds = useAppSelector(state => state.timelines[`account:${path}`]?.items || []);
|
||||||
const featuredStatusIds = useAppSelector(state => state.timelines.get(`account:${account?.id}:pinned`)?.items || ImmutableOrderedSet<string>());
|
const featuredStatusIds = useAppSelector(state => state.timelines[`account:${account?.id}:pinned`]?.items || []);
|
||||||
|
|
||||||
const isLoading = useAppSelector(state => state.timelines.get(`account:${path}`)?.isLoading === true);
|
const isLoading = useAppSelector(state => state.timelines[`account:${path}`]?.isLoading === true);
|
||||||
const hasMore = useAppSelector(state => state.timelines.get(`account:${path}`)?.hasMore === true);
|
const hasMore = useAppSelector(state => state.timelines[`account:${path}`]?.hasMore === true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (account && !withReplies) {
|
if (account && !withReplies) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import { useStatus } from 'pl-hooks';
|
import { useStatus } from 'pl-hooks';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
@ -30,15 +29,15 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
|
||||||
const { data: status } = useStatus(props.params.statusId);
|
const { data: status } = useStatus(props.params.statusId);
|
||||||
|
|
||||||
const descendantsIds = useAppSelector(state => {
|
const descendantsIds = useAppSelector(state => {
|
||||||
let descendantsIds = ImmutableOrderedSet<string>();
|
let descendantsIds = new Set<string>();
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
const statusId = status.id;
|
const statusId = status.id;
|
||||||
descendantsIds = getDescendantsIds(state, statusId);
|
descendantsIds = getDescendantsIds(state, statusId);
|
||||||
descendantsIds = descendantsIds.delete(statusId);
|
descendantsIds.delete(statusId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return descendantsIds;
|
return Array.from(descendantsIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
|
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
|
||||||
|
@ -60,12 +59,12 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
|
||||||
}, [props.params.statusId]);
|
}, [props.params.statusId]);
|
||||||
|
|
||||||
const handleMoveUp = (id: string) => {
|
const handleMoveUp = (id: string) => {
|
||||||
const index = ImmutableList(descendantsIds).indexOf(id);
|
const index = descendantsIds.indexOf(id);
|
||||||
_selectChild(index - 1);
|
_selectChild(index - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoveDown = (id: string) => {
|
const handleMoveDown = (id: string) => {
|
||||||
const index = ImmutableList(descendantsIds).indexOf(id);
|
const index = descendantsIds.indexOf(id);
|
||||||
_selectChild(index + 1);
|
_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')) {
|
if (id.endsWith('-tombstone')) {
|
||||||
return renderTombstone(id);
|
return renderTombstone(id);
|
||||||
} else {
|
} else {
|
||||||
|
@ -105,7 +104,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasDescendants = descendantsIds.size > 0;
|
const hasDescendants = descendantsIds.length > 0;
|
||||||
|
|
||||||
if (!status && isLoaded) {
|
if (!status && isLoaded) {
|
||||||
return (
|
return (
|
||||||
|
@ -120,7 +119,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
|
||||||
const children: JSX.Element[] = [];
|
const children: JSX.Element[] = [];
|
||||||
|
|
||||||
if (hasDescendants) {
|
if (hasDescendants) {
|
||||||
children.push(...renderChildren(descendantsIds).toArray());
|
children.push(...renderChildren(descendantsIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import { useStatusQuotes } from 'pl-hooks';
|
import { useStatusQuotes } from 'pl-hooks';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
@ -15,9 +14,7 @@ const Quotes: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { statusId } = useParams<{ statusId: string }>();
|
const { statusId } = useParams<{ statusId: string }>();
|
||||||
|
|
||||||
const { data, isLoading, hasNextPage, fetchNextPage } = useStatusQuotes(statusId);
|
const { data: statusIds = [], isLoading, hasNextPage, fetchNextPage } = useStatusQuotes(statusId);
|
||||||
|
|
||||||
const statusIds = ImmutableOrderedSet(data);
|
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.quotes' defaultMessage='This post has not been quoted yet.' />;
|
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 { createSelector } from '@reduxjs/toolkit';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
@ -26,11 +25,11 @@ export const getAncestorsIds = createSelector([
|
||||||
(_: RootState, statusId: string | undefined) => statusId,
|
(_: RootState, statusId: string | undefined) => statusId,
|
||||||
(state: RootState) => state.contexts.inReplyTos,
|
(state: RootState) => state.contexts.inReplyTos,
|
||||||
], (statusId, inReplyTos) => {
|
], (statusId, inReplyTos) => {
|
||||||
let ancestorsIds = ImmutableOrderedSet<string>();
|
let ancestorsIds = new Set<string>();
|
||||||
let id: string | undefined = statusId;
|
let id: string | undefined = statusId;
|
||||||
|
|
||||||
while (id && !ancestorsIds.includes(id)) {
|
while (id && !ancestorsIds.has(id)) {
|
||||||
ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds);
|
ancestorsIds = new Set([id]).union(ancestorsIds);
|
||||||
id = inReplyTos.get(id);
|
id = inReplyTos.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +40,7 @@ export const getDescendantsIds = createSelector([
|
||||||
(_: RootState, statusId: string) => statusId,
|
(_: RootState, statusId: string) => statusId,
|
||||||
(state: RootState) => state.contexts.replies,
|
(state: RootState) => state.contexts.replies,
|
||||||
], (statusId, contextReplies) => {
|
], (statusId, contextReplies) => {
|
||||||
let descendantsIds = ImmutableOrderedSet<string>();
|
let descendantsIds = new Set<string>();
|
||||||
const ids = [statusId];
|
const ids = [statusId];
|
||||||
|
|
||||||
while (ids.length > 0) {
|
while (ids.length > 0) {
|
||||||
|
@ -50,12 +49,12 @@ export const getDescendantsIds = createSelector([
|
||||||
|
|
||||||
const replies = contextReplies.get(id);
|
const replies = contextReplies.get(id);
|
||||||
|
|
||||||
if (descendantsIds.includes(id)) {
|
if (descendantsIds.has(id)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusId !== id) {
|
if (statusId !== id) {
|
||||||
descendantsIds = descendantsIds.union([id]);
|
descendantsIds = descendantsIds.union(new Set(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replies) {
|
if (replies) {
|
||||||
|
@ -91,26 +90,29 @@ const Thread: React.FC<IThread> = ({
|
||||||
const showMedia = statusesMeta[status.id]?.visible ?? !status.sensitive;
|
const showMedia = statusesMeta[status.id]?.visible ?? !status.sensitive;
|
||||||
|
|
||||||
const { ancestorsIds, descendantsIds } = useAppSelector((state) => {
|
const { ancestorsIds, descendantsIds } = useAppSelector((state) => {
|
||||||
let ancestorsIds = ImmutableOrderedSet<string>();
|
let ancestorsIds = new Set<string>();
|
||||||
let descendantsIds = ImmutableOrderedSet<string>();
|
let descendantsIds = new Set<string>();
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
const statusId = status.id;
|
const statusId = status.id;
|
||||||
ancestorsIds = getAncestorsIds(state, state.contexts.inReplyTos.get(statusId));
|
ancestorsIds = getAncestorsIds(state, state.contexts.inReplyTos.get(statusId));
|
||||||
descendantsIds = getDescendantsIds(state, 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 {
|
return {
|
||||||
status,
|
status,
|
||||||
ancestorsIds,
|
ancestorsIds: Array.from(ancestorsIds),
|
||||||
descendantsIds,
|
descendantsIds: Array.from(descendantsIds),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
let initialTopMostItemIndex = ancestorsIds.size;
|
let initialTopMostItemIndex = ancestorsIds.length;
|
||||||
if (!useWindowScroll && initialTopMostItemIndex !== 0) initialTopMostItemIndex = ancestorsIds.size + 1;
|
if (!useWindowScroll && initialTopMostItemIndex !== 0) initialTopMostItemIndex = ancestorsIds.length + 1;
|
||||||
|
|
||||||
const node = useRef<HTMLDivElement>(null);
|
const node = useRef<HTMLDivElement>(null);
|
||||||
const statusRef = useRef<HTMLDivElement>(null);
|
const statusRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -161,13 +163,13 @@ const Thread: React.FC<IThread> = ({
|
||||||
|
|
||||||
const handleMoveUp = (id: string) => {
|
const handleMoveUp = (id: string) => {
|
||||||
if (id === status?.id) {
|
if (id === status?.id) {
|
||||||
_selectChild(ancestorsIds.size - 1);
|
_selectChild(ancestorsIds.length - 1);
|
||||||
} else {
|
} else {
|
||||||
let index = ImmutableList(ancestorsIds).indexOf(id);
|
let index = ancestorsIds.indexOf(id);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
index = ImmutableList(descendantsIds).indexOf(id);
|
index = descendantsIds.indexOf(id);
|
||||||
_selectChild(ancestorsIds.size + index);
|
_selectChild(ancestorsIds.length + index);
|
||||||
} else {
|
} else {
|
||||||
_selectChild(index - 1);
|
_selectChild(index - 1);
|
||||||
}
|
}
|
||||||
|
@ -176,13 +178,13 @@ const Thread: React.FC<IThread> = ({
|
||||||
|
|
||||||
const handleMoveDown = (id: string) => {
|
const handleMoveDown = (id: string) => {
|
||||||
if (id === status?.id) {
|
if (id === status?.id) {
|
||||||
_selectChild(ancestorsIds.size + 1);
|
_selectChild(ancestorsIds.length + 1);
|
||||||
} else {
|
} else {
|
||||||
let index = ImmutableList(ancestorsIds).indexOf(id);
|
let index = ancestorsIds.indexOf(id);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
index = ImmutableList(descendantsIds).indexOf(id);
|
index = descendantsIds.indexOf(id);
|
||||||
_selectChild(ancestorsIds.size + index + 2);
|
_selectChild(ancestorsIds.length + index + 2);
|
||||||
} else {
|
} else {
|
||||||
_selectChild(index + 1);
|
_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')) {
|
if (id.endsWith('-tombstone')) {
|
||||||
return renderTombstone(id);
|
return renderTombstone(id);
|
||||||
} else {
|
} else {
|
||||||
|
@ -235,15 +237,15 @@ const Thread: React.FC<IThread> = ({
|
||||||
|
|
||||||
// Scroll focused status into view when thread updates.
|
// Scroll focused status into view when thread updates.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
virtualizer.current?.scrollToIndex(ancestorsIds.size);
|
virtualizer.current?.scrollToIndex(ancestorsIds.length);
|
||||||
}, [status.id, ancestorsIds.size]);
|
}, [status.id, ancestorsIds.length]);
|
||||||
|
|
||||||
const handleOpenCompareHistoryModal = (status: Pick<Status, 'id'>) => {
|
const handleOpenCompareHistoryModal = (status: Pick<Status, 'id'>) => {
|
||||||
openModal('COMPARE_HISTORY', { statusId: status.id });
|
openModal('COMPARE_HISTORY', { statusId: status.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAncestors = ancestorsIds.size > 0;
|
const hasAncestors = ancestorsIds.length > 0;
|
||||||
const hasDescendants = descendantsIds.size > 0;
|
const hasDescendants = descendantsIds.length > 0;
|
||||||
|
|
||||||
type HotkeyHandlers = { [key: string]: (keyEvent?: KeyboardEvent) => void };
|
type HotkeyHandlers = { [key: string]: (keyEvent?: KeyboardEvent) => void };
|
||||||
|
|
||||||
|
@ -291,7 +293,7 @@ const Thread: React.FC<IThread> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAncestors) {
|
if (hasAncestors) {
|
||||||
children.push(...renderChildren(ancestorsIds).toArray());
|
children.push(...renderChildren(ancestorsIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
children.push(focusedStatus);
|
children.push(focusedStatus);
|
||||||
|
@ -302,11 +304,11 @@ const Thread: React.FC<IThread> = ({
|
||||||
<FormattedMessage id='status.replies' defaultMessage='Replies' />
|
<FormattedMessage id='status.replies' defaultMessage='Replies' />
|
||||||
</p>,
|
</p>,
|
||||||
);
|
);
|
||||||
children.push(...renderChildren(descendantsIds).toArray());
|
children.push(...renderChildren(descendantsIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDescendants) {
|
if (hasDescendants) {
|
||||||
children.push(...renderChildren(descendantsIds).toArray());
|
children.push(...renderChildren(descendantsIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
||||||
import { useStatus } from 'pl-hooks';
|
import { useStatus } from 'pl-hooks';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
@ -35,17 +34,18 @@ const StatusDetails: React.FC<IStatusDetails> = (props) => {
|
||||||
const isLoaded = statusQuery.isSuccess;
|
const isLoaded = statusQuery.isSuccess;
|
||||||
|
|
||||||
const ancestorsIds = useAppSelector(state => {
|
const ancestorsIds = useAppSelector(state => {
|
||||||
let ancestorsIds = ImmutableOrderedSet<string>();
|
let ancestorsIds = new Set<string>();
|
||||||
let descendantsIds = ImmutableOrderedSet<string>();
|
let descendantsIds = new Set<string>();
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
const statusId = status.id;
|
const statusId = status.id;
|
||||||
ancestorsIds = getAncestorsIds(state, state.contexts.inReplyTos.get(statusId));
|
ancestorsIds = getAncestorsIds(state, state.contexts.inReplyTos.get(statusId));
|
||||||
descendantsIds = getDescendantsIds(state, 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. */
|
/** Fetch the status (and context) from the API. */
|
||||||
|
@ -60,9 +60,9 @@ const StatusDetails: React.FC<IStatusDetails> = (props) => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [props.params.statusId]);
|
}, [props.params.statusId]);
|
||||||
|
|
||||||
if (ancestorsIds.size) {
|
if (ancestorsIds.length) {
|
||||||
return (
|
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 React from 'react';
|
||||||
|
|
||||||
import StatusList, { IStatusList } from 'bigbuffet/components/status-list';
|
import StatusList, { IStatusList } from 'bigbuffet/components/status-list';
|
||||||
|
@ -15,15 +14,13 @@ const Timeline: React.FC<ITimeline> = ({
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const statusIds = useAppSelector(state => state.timelines.get(timelineId)?.items || ImmutableOrderedSet<string>());
|
const statusIds = useAppSelector(state => state.timelines[timelineId]?.items || []);
|
||||||
const lastStatusId = statusIds.last();
|
const isLoading = useAppSelector(state => (state.timelines[timelineId] || { isLoading: true }).isLoading === true);
|
||||||
const isLoading = useAppSelector(state => (state.timelines.get(timelineId) || { isLoading: true }).isLoading === true);
|
const hasMore = useAppSelector(state => state.timelines[timelineId]?.hasMore === true);
|
||||||
const hasMore = useAppSelector(state => state.timelines.get(timelineId)?.hasMore === true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusList
|
<StatusList
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
lastStatusId={lastStatusId}
|
|
||||||
statusIds={statusIds}
|
statusIds={statusIds}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
|
|
|
@ -21,7 +21,7 @@ type ContextStatus = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Import a single status into the reducer, setting replies and replyTos. */
|
/** 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;
|
const { id, in_reply_to_id: inReplyToId } = status;
|
||||||
if (!inReplyToId) return state;
|
if (!inReplyToId) return state;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { produce } from 'immer';
|
||||||
Map as ImmutableMap,
|
|
||||||
OrderedSet as ImmutableOrderedSet,
|
|
||||||
Record as ImmutableRecord,
|
|
||||||
} from 'immutable';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TIMELINE_CLEAR,
|
TIMELINE_CLEAR,
|
||||||
|
@ -14,36 +10,38 @@ import {
|
||||||
|
|
||||||
import type { PaginatedResponse, Status as BaseStatus } from 'pl-api';
|
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,
|
isLoading: false,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
next: null as (() => Promise<PaginatedResponse<BaseStatus>>) | null,
|
next: null,
|
||||||
prev: null as (() => Promise<PaginatedResponse<BaseStatus>>) | null,
|
prev: null,
|
||||||
items: ImmutableOrderedSet<string>(),
|
items: [],
|
||||||
queuedItems: ImmutableOrderedSet<string>(), //max= MAX_QUEUED_ITEMS
|
};
|
||||||
loadingFailed: false,
|
|
||||||
isPartial: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialState = ImmutableMap<string, Timeline>();
|
const initialState: State = {};
|
||||||
|
|
||||||
type State = ImmutableMap<string, Timeline>;
|
type State = Record<string, Timeline>;
|
||||||
type Timeline = ReturnType<typeof TimelineRecord>;
|
|
||||||
|
|
||||||
const getStatusIds = (statuses: Array<Pick<BaseStatus, 'id'>> = []) => (
|
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>()) => (
|
const mergeStatusIds = (oldIds = new Array<string>(), newIds = new Array<string>()) => (
|
||||||
newIds.union(oldIds)
|
Array.from(new Set(newIds).union(new Set(oldIds)))
|
||||||
);
|
);
|
||||||
|
|
||||||
const setLoading = (state: State, timelineId: string, loading: boolean) =>
|
const setLoading = (state: State, timelineId: string, loading: boolean) => {
|
||||||
state.update(timelineId, TimelineRecord(), timeline => timeline.set('isLoading', loading));
|
if (!state[timelineId]) state[timelineId] = emptyTimeline;
|
||||||
|
state[timelineId].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 expandNormalizedTimeline = (
|
const expandNormalizedTimeline = (
|
||||||
state: State,
|
state: State,
|
||||||
|
@ -51,37 +49,37 @@ const expandNormalizedTimeline = (
|
||||||
statuses: Array<BaseStatus>,
|
statuses: Array<BaseStatus>,
|
||||||
next: (() => Promise<PaginatedResponse<BaseStatus>>) | null,
|
next: (() => Promise<PaginatedResponse<BaseStatus>>) | null,
|
||||||
prev: (() => Promise<PaginatedResponse<BaseStatus>>) | null,
|
prev: (() => Promise<PaginatedResponse<BaseStatus>>) | null,
|
||||||
isPartial: boolean,
|
|
||||||
isLoadingRecent: boolean,
|
isLoadingRecent: boolean,
|
||||||
pos: 'start' | 'end' = 'end',
|
pos: 'start' | 'end' = 'end',
|
||||||
) => {
|
) => {
|
||||||
const newIds = getStatusIds(statuses);
|
const newIds = getStatusIds(statuses);
|
||||||
|
|
||||||
return state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => {
|
if (!state[timelineId]) state[timelineId] = emptyTimeline;
|
||||||
timeline.set('isLoading', false);
|
|
||||||
timeline.set('loadingFailed', false);
|
|
||||||
timeline.set('isPartial', isPartial);
|
|
||||||
timeline.set('next', next);
|
|
||||||
timeline.set('prev', prev);
|
|
||||||
|
|
||||||
if (!next && !isLoadingRecent) timeline.set('hasMore', false);
|
const timeline = state[timelineId];
|
||||||
|
|
||||||
// Pinned timelines can be replaced entirely
|
timeline.isLoading = false;
|
||||||
if (timelineId.endsWith(':pinned')) {
|
timeline.next = next;
|
||||||
timeline.set('items', newIds);
|
timeline.prev = prev;
|
||||||
return;
|
|
||||||
|
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.items = newItems;
|
||||||
timeline.update('items', oldIds => {
|
}
|
||||||
if (pos === 'end') {
|
|
||||||
return mergeStatusIds(newIds, oldIds);
|
|
||||||
} else {
|
|
||||||
return mergeStatusIds(oldIds, newIds);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// const shouldDelete = (timelineId: string, excludeAccount?: string | null) => {
|
// 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) =>
|
const handleExpandFail = (state: State, timelineId: string) => {
|
||||||
state.withMutations(state => {
|
setLoading(state, timelineId, false);
|
||||||
setLoading(state, timelineId, false);
|
return state;
|
||||||
setFailed(state, timelineId, true);
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const timelines = (state: State = initialState, action: TimelineAction) => {
|
const timelines = (state: State = initialState, action: TimelineAction) => produce(state, (draft) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case TIMELINE_EXPAND_REQUEST:
|
case TIMELINE_EXPAND_REQUEST:
|
||||||
return setLoading(state, action.timeline, true);
|
return setLoading(draft, action.timeline, true);
|
||||||
case TIMELINE_EXPAND_FAIL:
|
case TIMELINE_EXPAND_FAIL:
|
||||||
return handleExpandFail(state, action.timeline);
|
return handleExpandFail(draft, action.timeline);
|
||||||
case TIMELINE_EXPAND_SUCCESS:
|
case TIMELINE_EXPAND_SUCCESS:
|
||||||
return expandNormalizedTimeline(
|
return expandNormalizedTimeline(
|
||||||
state,
|
draft,
|
||||||
action.timeline,
|
action.timeline,
|
||||||
action.statuses,
|
action.statuses,
|
||||||
action.next,
|
action.next,
|
||||||
action.prev,
|
action.prev,
|
||||||
action.partial,
|
|
||||||
action.isLoadingRecent,
|
action.isLoadingRecent,
|
||||||
);
|
);
|
||||||
case TIMELINE_CLEAR:
|
case TIMELINE_CLEAR:
|
||||||
return clearTimeline(state, action.timeline);
|
return clearTimeline(draft, action.timeline);
|
||||||
default:
|
default:
|
||||||
return state;
|
return draft;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
export default timelines;
|
export default timelines;
|
||||||
|
|
Loading…
Reference in a new issue