pl-fe: migrate status lists to mutative

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-10 12:11:09 +01:00
parent fad9714475
commit b2d5bbf537
16 changed files with 142 additions and 146 deletions

View file

@ -17,7 +17,7 @@ const noOp = () => new Promise(f => f(undefined));
const fetchBookmarkedStatuses = (folderId?: string) => const fetchBookmarkedStatuses = (folderId?: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.get(folderId ? `bookmarks:${folderId}` : 'bookmarks')?.isLoading) { if (getState().status_lists[folderId ? `bookmarks:${folderId}` : 'bookmarks']?.isLoading) {
return dispatch(noOp); return dispatch(noOp);
} }
@ -52,9 +52,9 @@ const fetchBookmarkedStatusesFail = (error: unknown, folderId?: string) => ({
const expandBookmarkedStatuses = (folderId?: string) => const expandBookmarkedStatuses = (folderId?: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const list = folderId ? `bookmarks:${folderId}` : 'bookmarks'; const list = folderId ? `bookmarks:${folderId}` : 'bookmarks';
const next = getState().status_lists.get(list)?.next || null; const next = getState().status_lists[list]?.next || null;
if (next === null || getState().status_lists.get(list)?.isLoading) { if (next === null || getState().status_lists[list]?.isLoading) {
return dispatch(noOp); return dispatch(noOp);
} }

View file

@ -448,7 +448,7 @@ const initEventEdit = (statusId: string) => (dispatch: AppDispatch, getState: ()
const fetchRecentEvents = () => const fetchRecentEvents = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.get('recent_events')?.isLoading) { if (getState().status_lists.recent_events?.isLoading) {
return; return;
} }
@ -470,7 +470,7 @@ const fetchRecentEvents = () =>
const fetchJoinedEvents = () => const fetchJoinedEvents = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.get('joined_events')?.isLoading) { if (getState().status_lists.joined_events?.isLoading) {
return; return;
} }

View file

@ -27,7 +27,7 @@ const fetchFavouritedStatuses = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
if (getState().status_lists.get('favourites')?.isLoading) { if (getState().status_lists.favourites?.isLoading) {
return; return;
} }
@ -60,9 +60,9 @@ const expandFavouritedStatuses = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
const next = getState().status_lists.get('favourites')?.next || null; const next = getState().status_lists.favourites?.next || null;
if (next === null || getState().status_lists.get('favourites')?.isLoading) { if (next === null || getState().status_lists.favourites?.isLoading) {
return; return;
} }
@ -95,7 +95,7 @@ const fetchAccountFavouritedStatuses = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
if (getState().status_lists.get(`favourites:${accountId}`)?.isLoading) { if (getState().status_lists[`favourites:${accountId}`]?.isLoading) {
return; return;
} }
@ -131,9 +131,9 @@ const expandAccountFavouritedStatuses = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
const next = getState().status_lists.get(`favourites:${accountId}`)?.next || null; const next = getState().status_lists[`favourites:${accountId}`]?.next || null;
if (next === null || getState().status_lists.get(`favourites:${accountId}`)?.isLoading) { if (next === null || getState().status_lists[`favourites:${accountId}`]?.isLoading) {
return; return;
} }

View file

@ -19,7 +19,7 @@ const fetchScheduledStatuses = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
if (state.status_lists.get('scheduled_statuses')?.isLoading) { if (state.status_lists.scheduled_statuses?.isLoading) {
return; return;
} }
@ -63,9 +63,9 @@ const fetchScheduledStatusesFail = (error: unknown) => ({
const expandScheduledStatuses = () => const expandScheduledStatuses = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const next = getState().status_lists.get('scheduled_statuses')?.next as any as () => Promise<PaginatedResponse<ScheduledStatus>> || null; const next = getState().status_lists.scheduled_statuses?.next as any as () => Promise<PaginatedResponse<ScheduledStatus>> || null;
if (next === null || getState().status_lists.get('scheduled_statuses')?.isLoading) { if (next === null || getState().status_lists.scheduled_statuses?.isLoading) {
return; return;
} }

View file

@ -35,7 +35,7 @@ interface FetchStatusQuotesFailAction {
const fetchStatusQuotes = (statusId: string) => const fetchStatusQuotes = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) { if (getState().status_lists[`quotes:${statusId}`]?.isLoading) {
return dispatch(noOp); return dispatch(noOp);
} }
@ -78,9 +78,9 @@ interface ExpandStatusQuotesFailAction {
const expandStatusQuotes = (statusId: string) => const expandStatusQuotes = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const next = getState().status_lists.get(`quotes:${statusId}`)?.next || null; const next = getState().status_lists[`quotes:${statusId}`]?.next || null;
if (next === null || getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) { if (next === null || getState().status_lists[`quotes:${statusId}`]?.isLoading) {
return dispatch(noOp); return dispatch(noOp);
} }

View file

@ -1,5 +1,4 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -18,11 +17,11 @@ interface IStatusList extends Omit<IScrollableListWithContainer, 'onLoadMore' |
/** Unique key to preserve the scroll position when navigating back. */ /** Unique key to preserve the scroll position when navigating back. */
scrollKey: string; scrollKey: string;
/** 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. */
@ -57,13 +56,13 @@ const StatusList: React.FC<IStatusList> = ({
}) => { }) => {
const plFeConfig = usePlFeConfig(); const plFeConfig = usePlFeConfig();
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();
} }
}; };
@ -78,11 +77,11 @@ const StatusList: React.FC<IStatusList> = ({
}; };
const handleLoadOlder = useCallback(debounce(() => { const handleLoadOlder = useCallback(debounce(() => {
const maxId = lastStatusId || statusIds.last(); const maxId = lastStatusId || 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.at(-1)]);
const selectChild = (index: number) => { const selectChild = (index: number) => {
const selector = `#status-list [data-index="${index}"] .focusable`; const selector = `#status-list [data-index="${index}"] .focusable`;
@ -92,9 +91,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;
@ -136,7 +135,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}
@ -160,8 +159,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) {
@ -214,7 +213,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 variant={divideType === 'border' ? 'slim' : 'rounded'} />} placeholderComponent={() => <PlaceholderStatus variant={divideType === 'border' ? 'slim' : 'rounded'} />}
placeholderCount={20} placeholderCount={20}

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React from 'react'; import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@ -54,9 +53,9 @@ const Bookmarks: React.FC<IBookmarks> = ({ params }) => {
const bookmarksKey = folderId ? `bookmarks:${folderId}` : 'bookmarks'; const bookmarksKey = folderId ? `bookmarks:${folderId}` : 'bookmarks';
const statusIds = useAppSelector((state) => state.status_lists.get(bookmarksKey)?.items || ImmutableOrderedSet<string>()); const statusIds = useAppSelector((state) => state.status_lists[bookmarksKey]?.items || []);
const isLoading = useAppSelector((state) => state.status_lists.get(bookmarksKey)?.isLoading === true); const isLoading = useAppSelector((state) => state.status_lists[bookmarksKey]?.isLoading === true);
const hasMore = useAppSelector((state) => !!state.status_lists.get(bookmarksKey)?.next); const hasMore = useAppSelector((state) => !!state.status_lists[bookmarksKey]?.next);
React.useEffect(() => { React.useEffect(() => {
dispatch(fetchBookmarkedStatuses(folderId)); dispatch(fetchBookmarkedStatuses(folderId));

View file

@ -10,8 +10,6 @@ import { makeGetStatus } from 'pl-fe/selectors';
import PlaceholderEventPreview from '../../placeholder/components/placeholder-event-preview'; import PlaceholderEventPreview from '../../placeholder/components/placeholder-event-preview';
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
const Event = ({ id }: { id: string }) => { const Event = ({ id }: { id: string }) => {
const getStatus = useCallback(makeGetStatus(), []); const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector(state => getStatus(state, { id })); const status = useAppSelector(state => getStatus(state, { id }));
@ -29,7 +27,7 @@ const Event = ({ id }: { id: string }) => {
}; };
interface IEventCarousel { interface IEventCarousel {
statusIds: ImmutableOrderedSet<string>; statusIds: Array<string>;
isLoading?: boolean | null; isLoading?: boolean | null;
emptyMessage: React.ReactNode; emptyMessage: React.ReactNode;
} }
@ -38,10 +36,10 @@ const EventCarousel: React.FC<IEventCarousel> = ({ statusIds, isLoading, emptyMe
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
const handleChangeIndex = (index: number) => { const handleChangeIndex = (index: number) => {
setIndex(index % statusIds.size); setIndex(index % statusIds.length);
}; };
if (statusIds.size === 0) { if (statusIds.length === 0) {
if (isLoading) { if (isLoading) {
return <PlaceholderEventPreview />; return <PlaceholderEventPreview />;
} }
@ -67,7 +65,7 @@ const EventCarousel: React.FC<IEventCarousel> = ({ statusIds, isLoading, emptyMe
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}> <ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
{statusIds.map(statusId => <Event key={statusId} id={statusId} />)} {statusIds.map(statusId => <Event key={statusId} id={statusId} />)}
</ReactSwipeableViews> </ReactSwipeableViews>
{index !== statusIds.size - 1 && ( {index !== statusIds.length - 1 && (
<div className='absolute right-3 top-1/2 z-10 -mt-4'> <div className='absolute right-3 top-1/2 z-10 -mt-4'>
<button <button
onClick={() => handleChangeIndex(index + 1)} onClick={() => handleChangeIndex(index + 1)}

View file

@ -20,10 +20,10 @@ const Events = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const recentEvents = useAppSelector((state) => state.status_lists.get('recent_events')!.items); const recentEvents = useAppSelector((state) => state.status_lists.recent_events!.items);
const recentEventsLoading = useAppSelector((state) => state.status_lists.get('recent_events')!.isLoading); const recentEventsLoading = useAppSelector((state) => state.status_lists.recent_events!.isLoading);
const joinedEvents = useAppSelector((state) => state.status_lists.get('joined_events')!.items); const joinedEvents = useAppSelector((state) => state.status_lists.joined_events!.items);
const joinedEventsLoading = useAppSelector((state) => state.status_lists.get('joined_events')!.isLoading); const joinedEventsLoading = useAppSelector((state) => state.status_lists.joined_events!.isLoading);
useEffect(() => { useEffect(() => {
dispatch(fetchRecentEvents()); dispatch(fetchRecentEvents());

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@ -34,9 +33,9 @@ const Favourites: React.FC<IFavourites> = ({ params }) => {
const isOwnAccount = username.toLowerCase() === ownAccount?.acct?.toLowerCase(); const isOwnAccount = username.toLowerCase() === ownAccount?.acct?.toLowerCase();
const timelineKey = isOwnAccount ? 'favourites' : `favourites:${account?.id}`; const timelineKey = isOwnAccount ? 'favourites' : `favourites:${account?.id}`;
const statusIds = useAppSelector(state => state.status_lists.get(timelineKey)?.items || ImmutableOrderedSet<string>()); const statusIds = useAppSelector(state => state.status_lists[timelineKey]?.items || []);
const isLoading = useAppSelector(state => state.status_lists.get(timelineKey)?.isLoading === true); const isLoading = useAppSelector(state => state.status_lists[timelineKey]?.isLoading === true);
const hasMore = useAppSelector(state => !!state.status_lists.get(timelineKey)?.next); const hasMore = useAppSelector(state => !!state.status_lists[timelineKey]?.next);
const handleLoadMore = useCallback(debounce(() => { const handleLoadMore = useCallback(debounce(() => {
if (isOwnAccount) { if (isOwnAccount) {

View file

@ -20,9 +20,9 @@ const PinnedStatuses = () => {
const { username } = useParams<{ username: string }>(); const { username } = useParams<{ username: string }>();
const meUsername = useAppSelector((state) => selectOwnAccount(state)?.username || ''); const meUsername = useAppSelector((state) => selectOwnAccount(state)?.username || '');
const statusIds = useAppSelector((state) => state.status_lists.get('pins')!.items); const statusIds = useAppSelector((state) => state.status_lists.pins!.items);
const isLoading = useAppSelector((state) => !!state.status_lists.get('pins')!.isLoading); const isLoading = useAppSelector((state) => !!state.status_lists.pins!.isLoading);
const hasMore = useAppSelector((state) => !!state.status_lists.get('pins')!.next); const hasMore = useAppSelector((state) => !!state.status_lists.pins!.next);
const isMyAccount = username.toLowerCase() === meUsername.toLowerCase(); const isMyAccount = username.toLowerCase() === meUsername.toLowerCase();

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React from 'react'; import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@ -26,9 +25,9 @@ const Quotes: React.FC = () => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const statusIds = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'items'], ImmutableOrderedSet<string>())); const statusIds = useAppSelector((state) => state.status_lists[`quotes:${statusId}`]?.items || []);
const isLoading = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'isLoading'], true)); const isLoading = useAppSelector((state) => state.status_lists[`quotes:${statusId}`]?.isLoading !== false);
const hasMore = useAppSelector((state) => !!state.status_lists.getIn([`quotes:${statusId}`, 'next'])); const hasMore = useAppSelector((state) => !!state.status_lists[`quotes:${statusId}`]?.next);
React.useEffect(() => { React.useEffect(() => {
dispatch(fetchStatusQuotes(statusId)); dispatch(fetchStatusQuotes(statusId));
@ -41,7 +40,7 @@ const Quotes: React.FC = () => {
<StatusList <StatusList
className='black:p-0 black:sm:p-4 black:sm:pt-0' className='black:p-0 black:sm:p-4 black:sm:pt-0'
loadMoreClassName='black:sm:mx-4' loadMoreClassName='black:sm:mx-4'
statusIds={statusIds as ImmutableOrderedSet<string>} statusIds={statusIds}
scrollKey={`quotes:${statusId}`} scrollKey={`quotes:${statusId}`}
hasMore={hasMore} hasMore={hasMore}
isLoading={typeof isLoading === 'boolean' ? isLoading : true} isLoading={typeof isLoading === 'boolean' ? isLoading : true}

View file

@ -22,9 +22,9 @@ const ScheduledStatuses = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const statusIds = useAppSelector((state) => state.status_lists.get('scheduled_statuses')!.items); const statusIds = useAppSelector((state) => state.status_lists.scheduled_statuses!.items);
const isLoading = useAppSelector((state) => state.status_lists.get('scheduled_statuses')!.isLoading); const isLoading = useAppSelector((state) => state.status_lists.scheduled_statuses!.isLoading);
const hasMore = useAppSelector((state) => !!state.status_lists.get('scheduled_statuses')!.next); const hasMore = useAppSelector((state) => !!state.status_lists.scheduled_statuses!.next);
useEffect(() => { useEffect(() => {
dispatch(fetchScheduledStatuses()); dispatch(fetchScheduledStatuses());

View file

@ -34,7 +34,7 @@ const Timeline: React.FC<ITimeline> = ({
const getStatusIds = useCallback(makeGetStatusIds(), []); const getStatusIds = useCallback(makeGetStatusIds(), []);
const statusIds = useAppSelector(state => getStatusIds(state, { type: timelineId, prefix })); const statusIds = useAppSelector(state => getStatusIds(state, { type: timelineId, prefix }));
const lastStatusId = statusIds.last(); const lastStatusId = statusIds.at(-1);
const isLoading = useAppSelector(state => (state.timelines.get(timelineId) || { isLoading: true }).isLoading === true); const isLoading = useAppSelector(state => (state.timelines.get(timelineId) || { isLoading: true }).isLoading === true);
const isPartial = useAppSelector(state => (state.timelines.get(timelineId)?.isPartial || false) === true); const isPartial = useAppSelector(state => (state.timelines.get(timelineId)?.isPartial || false) === true);
const hasMore = useAppSelector(state => state.timelines.get(timelineId)?.hasMore === true); const hasMore = useAppSelector(state => state.timelines.get(timelineId)?.hasMore === true);

View file

@ -1,8 +1,4 @@
import { import { create } from 'mutative';
Map as ImmutableMap,
OrderedSet as ImmutableOrderedSet,
Record as ImmutableRecord,
} from 'immutable';
import { import {
STATUS_QUOTES_EXPAND_FAIL, STATUS_QUOTES_EXPAND_FAIL,
@ -72,62 +68,71 @@ import {
import type { PaginatedResponse, ScheduledStatus, Status } from 'pl-api'; import type { PaginatedResponse, ScheduledStatus, Status } from 'pl-api';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
const StatusListRecord = ImmutableRecord({ interface StatusList {
next: null as (() => Promise<PaginatedResponse<Status>>) | null, next: (() => Promise<PaginatedResponse<Status>>) | null;
loaded: boolean;
isLoading: boolean | null;
items: Array<string>;
}
const newStatusList = (): StatusList => ({
next: null,
loaded: false, loaded: false,
isLoading: null as boolean | null, isLoading: null,
items: ImmutableOrderedSet<string>(), items: [],
}); });
type State = ImmutableMap<string, StatusList>; type State = Record<string, StatusList>;
type StatusList = ReturnType<typeof StatusListRecord>;
const initialState: State = ImmutableMap({ const initialState: State = {
favourites: StatusListRecord(), favourites: newStatusList(),
bookmarks: StatusListRecord(), bookmarks: newStatusList(),
pins: StatusListRecord(), pins: newStatusList(),
scheduled_statuses: StatusListRecord(), scheduled_statuses: newStatusList(),
recent_events: StatusListRecord(), recent_events: newStatusList(),
joined_events: StatusListRecord(), joined_events: newStatusList(),
}); };
const getStatusId = (status: string | Pick<Status, 'id'>) => typeof status === 'string' ? status : status.id; const getStatusId = (status: string | Pick<Status, 'id'>) => typeof status === 'string' ? status : status.id;
const getStatusIds = (statuses: Array<string | Pick<Status, 'id'>> = []) => ( const getStatusIds = (statuses: Array<string | Pick<Status, 'id'>> = []) => statuses.map(getStatusId);
ImmutableOrderedSet(statuses.map(getStatusId))
);
const setLoading = (state: State, listType: string, loading: boolean) => const setLoading = (state: State, listType: string, loading: boolean) => {
state.update(listType, StatusListRecord(), listMap => listMap.set('isLoading', loading)); const list = state[listType] = state[listType] || newStatusList();
list.isLoading = loading;
};
const normalizeList = (state: State, listType: string, statuses: Array<string | Pick<Status, 'id'>>, next: (() => Promise<PaginatedResponse<Status>>) | null) => const normalizeList = (state: State, listType: string, statuses: Array<string | Pick<Status, 'id'>>, next: (() => Promise<PaginatedResponse<Status>>) | null) => {
state.update(listType, StatusListRecord(), listMap => listMap.withMutations(map => { const list = state[listType] = state[listType] || newStatusList();
map.set('next', next);
map.set('loaded', true); list.next = next;
map.set('isLoading', false); list.loaded = true;
map.set('items', getStatusIds(statuses)); list.isLoading = false;
})); list.items = getStatusIds(statuses);
};
const appendToList = (state: State, listType: string, statuses: Array<string | Pick<Status, 'id'>>, next: (() => Promise<PaginatedResponse<Status>>) | null) => { const appendToList = (state: State, listType: string, statuses: Array<string | Pick<Status, 'id'>>, next: (() => Promise<PaginatedResponse<Status>>) | null) => {
const newIds = getStatusIds(statuses); const newIds = getStatusIds(statuses);
return state.update(listType, StatusListRecord(), listMap => listMap.withMutations(map => { const list = state[listType] = state[listType] || newStatusList();
map.set('next', next);
map.set('isLoading', false); list.next = next;
map.update('items', items => items.union(newIds)); list.isLoading = false;
})); list.items = [...new Set([...list.items, ...newIds])];
}; };
const prependOneToList = (state: State, listType: string, status: string | Pick<Status, 'id'>) => { const prependOneToList = (state: State, listType: string, status: string | Pick<Status, 'id'>) => {
const statusId = getStatusId(status); const statusId = getStatusId(status);
return state.update(listType, StatusListRecord(), listMap => listMap.update('items', items => const list = state[listType] = state[listType] || newStatusList();
ImmutableOrderedSet([statusId]).union(items as ImmutableOrderedSet<string>),
)); list.items = [...new Set([statusId, ...list.items])];
}; };
const removeOneFromList = (state: State, listType: string, status: string | Pick<Status, 'id'>) => { const removeOneFromList = (state: State, listType: string, status: string | Pick<Status, 'id'>) => {
const statusId = getStatusId(status); const statusId = getStatusId(status);
return state.update(listType, StatusListRecord(), listMap => listMap.update('items', items => items.delete(statusId))); const list = state[listType] = state[listType] || newStatusList();
list.items = list.items.filter(id => id !== statusId);
}; };
const maybeAppendScheduledStatus = (state: State, status: Pick<ScheduledStatus | Status, 'id' | 'scheduled_at'>) => { const maybeAppendScheduledStatus = (state: State, status: Pick<ScheduledStatus | Status, 'id' | 'scheduled_at'>) => {
@ -136,112 +141,109 @@ const maybeAppendScheduledStatus = (state: State, status: Pick<ScheduledStatus |
}; };
const addBookmarkToLists = (state: State, status: Pick<Status, 'id' | 'bookmark_folder'>) => { const addBookmarkToLists = (state: State, status: Pick<Status, 'id' | 'bookmark_folder'>) => {
state = prependOneToList(state, 'bookmarks', status); prependOneToList(state, 'bookmarks', status);
const folderId = status.bookmark_folder; const folderId = status.bookmark_folder;
if (folderId) { if (folderId) {
return prependOneToList(state, `bookmarks:${folderId}`, status); prependOneToList(state, `bookmarks:${folderId}`, status);
} }
return state;
}; };
const removeBookmarkFromLists = (state: State, status: Pick<Status, 'id' | 'bookmark_folder'>) => { const removeBookmarkFromLists = (state: State, status: Pick<Status, 'id' | 'bookmark_folder'>) => {
state = removeOneFromList(state, 'bookmarks', status); removeOneFromList(state, 'bookmarks', status);
const folderId = status.bookmark_folder; const folderId = status.bookmark_folder;
if (folderId) { if (folderId) {
return removeOneFromList(state, `bookmarks:${folderId}`, status); removeOneFromList(state, `bookmarks:${folderId}`, status);
} }
return state;
}; };
const statusLists = (state = initialState, action: AnyAction | BookmarksAction | EventsAction | FavouritesAction | InteractionsAction | PinStatusesAction | StatusQuotesAction) => { const statusLists = (state = initialState, action: AnyAction | BookmarksAction | EventsAction | FavouritesAction | InteractionsAction | PinStatusesAction | StatusQuotesAction): State => {
switch (action.type) { switch (action.type) {
case FAVOURITED_STATUSES_FETCH_REQUEST: case FAVOURITED_STATUSES_FETCH_REQUEST:
case FAVOURITED_STATUSES_EXPAND_REQUEST: case FAVOURITED_STATUSES_EXPAND_REQUEST:
return setLoading(state, 'favourites', true); return create(state, draft => setLoading(draft, 'favourites', true));
case FAVOURITED_STATUSES_FETCH_FAIL: case FAVOURITED_STATUSES_FETCH_FAIL:
case FAVOURITED_STATUSES_EXPAND_FAIL: case FAVOURITED_STATUSES_EXPAND_FAIL:
return setLoading(state, 'favourites', false); return create(state, draft => setLoading(draft, 'favourites', false));
case FAVOURITED_STATUSES_FETCH_SUCCESS: case FAVOURITED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, 'favourites', action.statuses, action.next); return create(state, draft => normalizeList(draft, 'favourites', action.statuses, action.next));
case FAVOURITED_STATUSES_EXPAND_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS:
return appendToList(state, 'favourites', action.statuses, action.next); return create(state, draft => appendToList(draft, 'favourites', action.statuses, action.next));
case ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST: case ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST:
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST: case ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST:
return setLoading(state, `favourites:${action.accountId}`, true); return create(state, draft => setLoading(draft, `favourites:${action.accountId}`, true));
case ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL: case ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL:
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL: case ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL:
return setLoading(state, `favourites:${action.accountId}`, false); return create(state, draft => setLoading(draft, `favourites:${action.accountId}`, false));
case ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS: case ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, `favourites:${action.accountId}`, action.statuses, action.next); return create(state, draft => normalizeList(draft, `favourites:${action.accountId}`, action.statuses, action.next));
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS: case ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS:
return appendToList(state, `favourites:${action.accountId}`, action.statuses, action.next); return create(state, draft => appendToList(draft, `favourites:${action.accountId}`, action.statuses, action.next));
case BOOKMARKED_STATUSES_FETCH_REQUEST: case BOOKMARKED_STATUSES_FETCH_REQUEST:
case BOOKMARKED_STATUSES_EXPAND_REQUEST: case BOOKMARKED_STATUSES_EXPAND_REQUEST:
return setLoading(state, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', true); return create(state, draft => setLoading(draft, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', true));
case BOOKMARKED_STATUSES_FETCH_FAIL: case BOOKMARKED_STATUSES_FETCH_FAIL:
case BOOKMARKED_STATUSES_EXPAND_FAIL: case BOOKMARKED_STATUSES_EXPAND_FAIL:
return setLoading(state, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', false); return create(state, draft => setLoading(draft, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', false));
case BOOKMARKED_STATUSES_FETCH_SUCCESS: case BOOKMARKED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', action.statuses, action.next); return create(state, draft => normalizeList(draft, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', action.statuses, action.next));
case BOOKMARKED_STATUSES_EXPAND_SUCCESS: case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
return appendToList(state, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', action.statuses, action.next); return create(state, draft => appendToList(draft, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', action.statuses, action.next));
case FAVOURITE_SUCCESS: case FAVOURITE_SUCCESS:
return prependOneToList(state, 'favourites', action.status); return create(state, draft => prependOneToList(draft, 'favourites', action.status));
case UNFAVOURITE_SUCCESS: case UNFAVOURITE_SUCCESS:
return removeOneFromList(state, 'favourites', action.status); return create(state, draft => removeOneFromList(draft, 'favourites', action.status));
case BOOKMARK_SUCCESS: case BOOKMARK_SUCCESS:
return addBookmarkToLists(state, action.status); return create(state, draft => addBookmarkToLists(draft, action.status));
case UNBOOKMARK_SUCCESS: case UNBOOKMARK_SUCCESS:
return removeBookmarkFromLists(state, action.status); return create(state, draft => removeBookmarkFromLists(draft, action.status));
case PINNED_STATUSES_FETCH_SUCCESS: case PINNED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, 'pins', action.statuses, action.next); return create(state, draft => normalizeList(draft, 'pins', action.statuses, action.next));
case PIN_SUCCESS: case PIN_SUCCESS:
return prependOneToList(state, 'pins', action.status); return create(state, draft => prependOneToList(draft, 'pins', action.status));
case UNPIN_SUCCESS: case UNPIN_SUCCESS:
return removeOneFromList(state, 'pins', action.status); return create(state, draft => removeOneFromList(draft, 'pins', action.status));
case SCHEDULED_STATUSES_FETCH_REQUEST: case SCHEDULED_STATUSES_FETCH_REQUEST:
case SCHEDULED_STATUSES_EXPAND_REQUEST: case SCHEDULED_STATUSES_EXPAND_REQUEST:
return setLoading(state, 'scheduled_statuses', true); return create(state, draft => setLoading(draft, 'scheduled_statuses', true));
case SCHEDULED_STATUSES_FETCH_FAIL: case SCHEDULED_STATUSES_FETCH_FAIL:
case SCHEDULED_STATUSES_EXPAND_FAIL: case SCHEDULED_STATUSES_EXPAND_FAIL:
return setLoading(state, 'scheduled_statuses', false); return create(state, draft => setLoading(draft, 'scheduled_statuses', false));
case SCHEDULED_STATUSES_FETCH_SUCCESS: case SCHEDULED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, 'scheduled_statuses', action.statuses, action.next); return create(state, draft => normalizeList(draft, 'scheduled_statuses', action.statuses, action.next));
case SCHEDULED_STATUSES_EXPAND_SUCCESS: case SCHEDULED_STATUSES_EXPAND_SUCCESS:
return appendToList(state, 'scheduled_statuses', action.statuses, action.next); return create(state, draft => appendToList(draft, 'scheduled_statuses', action.statuses, action.next));
case SCHEDULED_STATUS_CANCEL_REQUEST: case SCHEDULED_STATUS_CANCEL_REQUEST:
case SCHEDULED_STATUS_CANCEL_SUCCESS: case SCHEDULED_STATUS_CANCEL_SUCCESS:
return removeOneFromList(state, 'scheduled_statuses', action.statusId); return create(state, draft => removeOneFromList(draft, 'scheduled_statuses', action.statusId));
case STATUS_QUOTES_FETCH_REQUEST: case STATUS_QUOTES_FETCH_REQUEST:
case STATUS_QUOTES_EXPAND_REQUEST: case STATUS_QUOTES_EXPAND_REQUEST:
return setLoading(state, `quotes:${action.statusId}`, true); return create(state, draft => setLoading(draft, `quotes:${action.statusId}`, true));
case STATUS_QUOTES_FETCH_FAIL: case STATUS_QUOTES_FETCH_FAIL:
case STATUS_QUOTES_EXPAND_FAIL: case STATUS_QUOTES_EXPAND_FAIL:
return setLoading(state, `quotes:${action.statusId}`, false); return create(state, draft => setLoading(draft, `quotes:${action.statusId}`, false));
case STATUS_QUOTES_FETCH_SUCCESS: case STATUS_QUOTES_FETCH_SUCCESS:
return normalizeList(state, `quotes:${action.statusId}`, action.statuses, action.next); return create(state, draft => normalizeList(draft, `quotes:${action.statusId}`, action.statuses, action.next));
case STATUS_QUOTES_EXPAND_SUCCESS: case STATUS_QUOTES_EXPAND_SUCCESS:
return appendToList(state, `quotes:${action.statusId}`, action.statuses, action.next); return create(state, draft => appendToList(draft, `quotes:${action.statusId}`, action.statuses, action.next));
case RECENT_EVENTS_FETCH_REQUEST: case RECENT_EVENTS_FETCH_REQUEST:
return setLoading(state, 'recent_events', true); return create(state, draft => setLoading(draft, 'recent_events', true));
case RECENT_EVENTS_FETCH_FAIL: case RECENT_EVENTS_FETCH_FAIL:
return setLoading(state, 'recent_events', false); return create(state, draft => setLoading(draft, 'recent_events', false));
case RECENT_EVENTS_FETCH_SUCCESS: case RECENT_EVENTS_FETCH_SUCCESS:
return normalizeList(state, 'recent_events', action.statuses, action.next); return create(state, draft => normalizeList(draft, 'recent_events', action.statuses, action.next));
case JOINED_EVENTS_FETCH_REQUEST: case JOINED_EVENTS_FETCH_REQUEST:
return setLoading(state, 'joined_events', true); return create(state, draft => setLoading(draft, 'joined_events', true));
case JOINED_EVENTS_FETCH_FAIL: case JOINED_EVENTS_FETCH_FAIL:
return setLoading(state, 'joined_events', false); return create(state, draft => setLoading(draft, 'joined_events', false));
case JOINED_EVENTS_FETCH_SUCCESS: case JOINED_EVENTS_FETCH_SUCCESS:
return normalizeList(state, 'joined_events', action.statuses, action.next); return create(state, draft => normalizeList(draft, 'joined_events', action.statuses, action.next));
case STATUS_CREATE_SUCCESS: case STATUS_CREATE_SUCCESS:
return maybeAppendScheduledStatus(state, action.status); return create(state, draft => maybeAppendScheduledStatus(draft, action.status));
default: default:
return state; return state;
} }
}; };
export { export {
StatusListRecord,
statusLists as default, statusLists as default,
}; };

View file

@ -347,7 +347,7 @@ const makeGetStatusIds = () => createSelector([
const status = statuses[id]; const status = statuses[id];
if (!status) return true; if (!status) return true;
return !shouldFilter(status, columnSettings); return !shouldFilter(status, columnSettings);
}), }).toArray(),
); );
export { export {