pl-fe: Move user lists away from immutable

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-06 12:56:09 +01:00
parent 563e5288fb
commit 0811af7ad7
20 changed files with 197 additions and 157 deletions

View file

@ -43,7 +43,7 @@ const expandDirectory = (params: Record<string, any>) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
dispatch(expandDirectoryRequest()); dispatch(expandDirectoryRequest());
const loadedItems = getState().user_lists.directory.items.size; const loadedItems = getState().user_lists.directory.items.length;
return getClient(getState()).instance.profileDirectory({ ...params, offset: loadedItems, limit: 20 }).then((data) => { return getClient(getState()).instance.profileDirectory({ ...params, offset: loadedItems, limit: 20 }).then((data) => {
dispatch(importEntities({ accounts: data })); dispatch(importEntities({ accounts: data }));

View file

@ -268,7 +268,7 @@ const fetchEventParticipationsFail = (statusId: string, error: unknown) => ({
const expandEventParticipations = (statusId: string) => const expandEventParticipations = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const next = getState().user_lists.event_participations.get(statusId)?.next || null; const next = getState().user_lists.event_participations[statusId]?.next || null;
if (next === null) { if (next === null) {
return dispatch(noOp); return dispatch(noOp);
@ -337,7 +337,7 @@ const fetchEventParticipationRequestsFail = (statusId: string, error: unknown) =
const expandEventParticipationRequests = (statusId: string) => const expandEventParticipationRequests = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const next = getState().user_lists.event_participation_requests.get(statusId)?.next || null; const next = getState().user_lists.event_participation_requests[statusId]?.next || null;
if (next === null) { if (next === null) {
return dispatch(noOp); return dispatch(noOp);

View file

@ -28,7 +28,7 @@ const useAccountList = (listKey: string[], entityFn: EntityFn<void>) => {
getNextPageParam: (config) => config.next ? config : undefined, getNextPageParam: (config) => config.next ? config : undefined,
}); });
const data = flattenPages<Account>(queryInfo.data as any)?.toReversed() || []; const data = flattenPages<Account>(queryInfo.data as any) || [];
const { relationships } = useRelationships( const { relationships } = useRelationships(
listKey, listKey,

View file

@ -1,23 +1,18 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
import React from 'react'; import React from 'react';
import Avatar from 'pl-fe/components/ui/avatar'; import Avatar from 'pl-fe/components/ui/avatar';
import HStack from 'pl-fe/components/ui/hstack'; import HStack from 'pl-fe/components/ui/hstack';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { makeGetAccount } from 'pl-fe/selectors'; import { selectAccounts } from 'pl-fe/selectors';
import type { Account } from 'pl-fe/normalizers/account';
const getAccount = makeGetAccount();
interface IAvatarStack { interface IAvatarStack {
accountIds: ImmutableOrderedSet<string>; accountIds: Array<string>;
limit?: number; limit?: number;
} }
const AvatarStack: React.FC<IAvatarStack> = ({ accountIds, limit = 3 }) => { const AvatarStack: React.FC<IAvatarStack> = ({ accountIds, limit = 3 }) => {
const accounts = useAppSelector(state => ImmutableList(accountIds.slice(0, limit).map(accountId => getAccount(state, accountId)).filter(account => account))) as ImmutableList<Account>; const accounts = useAppSelector(state => selectAccounts(state, accountIds.slice(0, limit)));
return ( return (
<HStack className='relative' aria-hidden> <HStack className='relative' aria-hidden>

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -22,7 +21,7 @@ interface IBirthdayPanel {
const BirthdayPanel = ({ limit }: IBirthdayPanel) => { const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const birthdays: ImmutableOrderedSet<string> = useAppSelector(state => state.user_lists.birthday_reminders.get(state.me as string)?.items || ImmutableOrderedSet()); const birthdays = useAppSelector(state => state.user_lists.birthday_reminders[state.me as string]?.items || []);
const birthdaysToRender = birthdays.slice(0, limit); const birthdaysToRender = birthdays.slice(0, limit);
const timeout = useRef<NodeJS.Timeout>(); const timeout = useRef<NodeJS.Timeout>();
@ -48,7 +47,7 @@ const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
}; };
}, []); }, []);
if (birthdaysToRender.isEmpty()) { if (!birthdaysToRender.length) {
return null; return null;
} }

View file

@ -24,7 +24,6 @@ import { useSettingsStore } from 'pl-fe/stores/settings';
import { useUiStore } from 'pl-fe/stores/ui'; import { useUiStore } from 'pl-fe/stores/ui';
import sourceCode from 'pl-fe/utils/code'; import sourceCode from 'pl-fe/utils/code';
import type { List as ImmutableList } from 'immutable';
import type { Account as AccountEntity } from 'pl-fe/normalizers/account'; import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
const messages = defineMessages({ const messages = defineMessages({
@ -95,9 +94,9 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
const features = useFeatures(); const features = useFeatures();
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
const { account } = useAccount(me || undefined); const { account } = useAccount(me || undefined);
const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state)); const otherAccounts = useAppSelector((state) => getOtherAccounts(state));
const { settings } = useSettingsStore(); const { settings } = useSettingsStore();
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count()); const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.length);
const interactionRequestsCount = useInteractionRequestsCount().data || 0; const interactionRequestsCount = useInteractionRequestsCount().data || 0;
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const draftCount = useAppSelector((state) => state.draft_statuses.size); const draftCount = useAppSelector((state) => state.draft_statuses.size);

View file

@ -48,7 +48,7 @@ const SidebarNavigation = () => {
const logoSrc = useLogo(); const logoSrc = useLogo();
const notificationCount = useAppSelector((state) => state.notifications.unread); const notificationCount = useAppSelector((state) => state.notifications.unread);
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count()); const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.length);
const interactionRequestsCount = useInteractionRequestsCount().data || 0; const interactionRequestsCount = useInteractionRequestsCount().data || 0;
const dashboardCount = useAppSelector((state) => state.admin.openReports.length + state.admin.awaitingApproval.length); const dashboardCount = useAppSelector((state) => state.admin.openReports.length + state.admin.awaitingApproval.length);
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);

View file

@ -70,7 +70,7 @@ interface IManagePendingParticipants {
const ManagePendingParticipants: React.FC<IManagePendingParticipants> = ({ statusId }) => { const ManagePendingParticipants: React.FC<IManagePendingParticipants> = ({ statusId }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const accounts = useAppSelector((state) => state.user_lists.event_participation_requests.get(statusId!)?.items); const accounts = useAppSelector((state) => state.user_lists.event_participation_requests[statusId]?.items);
useEffect(() => { useEffect(() => {
if (statusId) dispatch(fetchEventParticipationRequests(statusId)); if (statusId) dispatch(fetchEventParticipationRequests(statusId));

View file

@ -66,7 +66,7 @@ const GroupBlockedMembers: React.FC<IGroupBlockedMembers> = ({ params }) => {
const groupId = params?.groupId; const groupId = params?.groupId;
const { group } = useGroup(groupId); const { group } = useGroup(groupId);
const accountIds = useAppSelector((state) => state.user_lists.group_blocks.get(groupId)?.items); const accountIds = useAppSelector((state) => state.user_lists.group_blocks[groupId]?.items);
useEffect(() => { useEffect(() => {
dispatch(fetchGroupBlocks(groupId)); dispatch(fetchGroupBlocks(groupId));

View file

@ -10,7 +10,7 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import type { BaseModalProps } from '../modal-root'; import type { BaseModalProps } from '../modal-root';
const BirthdaysModal = ({ onClose }: BaseModalProps) => { const BirthdaysModal = ({ onClose }: BaseModalProps) => {
const accountIds = useAppSelector(state => state.user_lists.birthday_reminders.get(state.me as string)?.items); const accountIds = useAppSelector(state => state.user_lists.birthday_reminders[state.me as string]?.items);
const onClickClose = () => { const onClickClose = () => {
onClose('BIRTHDAYS'); onClose('BIRTHDAYS');

View file

@ -18,7 +18,7 @@ interface DislikesModalProps {
const DislikesModal: React.FC<BaseModalProps & DislikesModalProps> = ({ onClose, statusId }) => { const DislikesModal: React.FC<BaseModalProps & DislikesModalProps> = ({ onClose, statusId }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const accountIds = useAppSelector((state) => state.user_lists.disliked_by.get(statusId)?.items); const accountIds = useAppSelector((state) => state.user_lists.disliked_by[statusId]?.items);
const fetchData = () => { const fetchData = () => {
dispatch(fetchDislikes(statusId)); dispatch(fetchDislikes(statusId));

View file

@ -18,7 +18,7 @@ interface EventParticipantsModalProps {
const EventParticipantsModal: React.FC<BaseModalProps & EventParticipantsModalProps> = ({ onClose, statusId }) => { const EventParticipantsModal: React.FC<BaseModalProps & EventParticipantsModalProps> = ({ onClose, statusId }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const accountIds = useAppSelector((state) => state.user_lists.event_participations.get(statusId)?.items); const accountIds = useAppSelector((state) => state.user_lists.event_participations[statusId]?.items);
const fetchData = () => { const fetchData = () => {
dispatch(fetchEventParticipations(statusId)); dispatch(fetchEventParticipations(statusId));

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -21,7 +20,7 @@ interface FamiliarFollowersModalProps {
const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalProps> = ({ accountId, onClose }) => { const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalProps> = ({ accountId, onClose }) => {
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
const account = useAppSelector(state => getAccount(state, accountId)); const account = useAppSelector(state => getAccount(state, accountId));
const familiarFollowerIds: ImmutableOrderedSet<string> = useAppSelector(state => state.user_lists.familiar_followers.get(accountId)?.items || ImmutableOrderedSet()); const familiarFollowerIds = useAppSelector(state => state.user_lists.familiar_followers[accountId]?.items || []);
const onClickClose = () => { const onClickClose = () => {
onClose('FAMILIAR_FOLLOWERS'); onClose('FAMILIAR_FOLLOWERS');

View file

@ -19,8 +19,8 @@ const FavouritesModal: React.FC<BaseModalProps & FavouritesModalProps> = ({ onCl
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const accountIds = useAppSelector((state) => state.user_lists.favourited_by.get(statusId)?.items); const accountIds = useAppSelector((state) => state.user_lists.favourited_by[statusId]?.items);
const next = useAppSelector((state) => state.user_lists.favourited_by.get(statusId)?.next); const next = useAppSelector((state) => state.user_lists.favourited_by[statusId]?.next);
const fetchData = () => { const fetchData = () => {
dispatch(fetchFavourites(statusId)); dispatch(fetchFavourites(statusId));

View file

@ -1,5 +1,4 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { List as ImmutableList } from 'immutable';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
@ -36,7 +35,7 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const [reaction, setReaction] = useState(initialReaction); const [reaction, setReaction] = useState(initialReaction);
const reactions = useAppSelector((state) => state.user_lists.reactions.get(statusId)?.items); const reactions = useAppSelector((state) => state.user_lists.reactions[statusId]?.items);
const onClickClose = () => { const onClickClose = () => {
onClose('REACTIONS'); onClose('REACTIONS');
@ -65,15 +64,15 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
return <Tabs items={items} activeItem={reaction || 'all'} />; return <Tabs items={items} activeItem={reaction || 'all'} />;
}; };
const accounts = useMemo((): ImmutableList<IAccountWithReaction> | undefined => { const accounts = useMemo((): Array<IAccountWithReaction> | undefined => {
if (!reactions) return; if (!reactions) return;
if (reaction) { if (reaction) {
const reactionRecord = reactions.find(({ name }) => name === reaction); const reactionRecord = reactions.find(({ name }) => name === reaction);
if (reactionRecord) return reactionRecord.accounts.map(account => ({ id: account, reaction: reaction, reactionUrl: reactionRecord.url || undefined })).toList(); if (reactionRecord) return reactionRecord.accounts.map(account => ({ id: account, reaction: reaction, reactionUrl: reactionRecord.url || undefined }));
} else { } else {
return reactions.map(({ accounts, name, url }) => accounts.map(account => ({ id: account, reaction: name, reactionUrl: url }))).flatten() as ImmutableList<IAccountWithReaction>; return reactions.map(({ accounts, name, url }) => accounts.map(account => ({ id: account, reaction: name, reactionUrl: url || undefined }))).flat();
} }
}, [reactions, reaction]); }, [reactions, reaction]);
@ -89,11 +88,11 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />; const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
body = (<> body = (<>
{reactions.size > 0 && renderFilterBar()} {reactions.length > 0 && renderFilterBar()}
<ScrollableList <ScrollableList
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
listClassName={clsx('max-w-full', { listClassName={clsx('max-w-full', {
'mt-4': reactions.size > 0, 'mt-4': reactions.length > 0,
})} })}
itemClassName='pb-3' itemClassName='pb-3'
style={{ height: 'calc(80vh - 88px)' }} style={{ height: 'calc(80vh - 88px)' }}

View file

@ -19,8 +19,8 @@ interface ReblogsModalProps {
const ReblogsModal: React.FC<BaseModalProps & ReblogsModalProps> = ({ onClose, statusId }) => { const ReblogsModal: React.FC<BaseModalProps & ReblogsModalProps> = ({ onClose, statusId }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const accountIds = useAppSelector((state) => state.user_lists.reblogged_by.get(statusId)?.items); const accountIds = useAppSelector((state) => state.user_lists.reblogged_by[statusId]?.items);
const next = useAppSelector((state) => state.user_lists.reblogged_by.get(statusId)?.next); const next = useAppSelector((state) => state.user_lists.reblogged_by[statusId]?.next);
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
const fetchData = () => { const fetchData = () => {

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -19,13 +18,13 @@ interface IPinnedAccountsPanel {
const PinnedAccountsPanel: React.FC<IPinnedAccountsPanel> = ({ account, limit }) => { const PinnedAccountsPanel: React.FC<IPinnedAccountsPanel> = ({ account, limit }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const pinned = useAppSelector((state) => state.user_lists.pinned.get(account.id)?.items || ImmutableOrderedSet<string>()).slice(0, limit); const pinned = useAppSelector((state) => state.user_lists.pinned[account.id]?.items || []).slice(0, limit);
useEffect(() => { useEffect(() => {
dispatch(fetchPinnedAccounts(account.id)); dispatch(fetchPinnedAccounts(account.id));
}, []); }, []);
if (pinned.isEmpty()) { if (!pinned.length) {
return ( return (
<WhoToFollowPanel limit={limit} /> <WhoToFollowPanel limit={limit} />
); );

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { FormattedList, FormattedMessage } from 'react-intl'; import { FormattedList, FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -29,8 +28,8 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
const features = useFeatures(); const features = useFeatures();
const familiarFollowerIds = useAppSelector(state => state.user_lists.familiar_followers.get(account.id)?.items || ImmutableOrderedSet<string>()); const familiarFollowerIds = useAppSelector(state => state.user_lists.familiar_followers[account.id]?.items || []);
const familiarFollowers: ImmutableOrderedSet<Account | null> = useAppSelector(state => familiarFollowerIds.slice(0, 2).map(accountId => getAccount(state, accountId))); const familiarFollowers = useAppSelector(state => familiarFollowerIds.slice(0, 2).map(accountId => getAccount(state, accountId)));
useEffect(() => { useEffect(() => {
if (me && features.familiarFollowers) { if (me && features.familiarFollowers) {
@ -44,7 +43,7 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
}); });
}; };
if (familiarFollowerIds.size === 0) { if (familiarFollowerIds.length === 0) {
return null; return null;
} }
@ -60,15 +59,15 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
</HStack> </HStack>
</Link> </Link>
</HoverAccountWrapper> </HoverAccountWrapper>
)).toArray().filter(Boolean); )).filter(Boolean);
if (familiarFollowerIds.size > 2) { if (familiarFollowerIds.length > 2) {
accounts.push( accounts.push(
<span key='_' className='cursor-pointer hover:underline' role='presentation' onClick={openFamiliarFollowersModal}> <span key='_' className='cursor-pointer hover:underline' role='presentation' onClick={openFamiliarFollowersModal}>
<FormattedMessage <FormattedMessage
id='account.familiar_followers.more' id='account.familiar_followers.more'
defaultMessage='{count, plural, one {# other} other {# others}} you follow' defaultMessage='{count, plural, one {# other} other {# others}} you follow'
values={{ count: familiarFollowerIds.size - familiarFollowers.size }} values={{ count: familiarFollowerIds.length - familiarFollowers.length }}
/> />
</span>, </span>,
); );

View file

@ -1,8 +1,4 @@
import { import { create } from 'mutative';
Map as ImmutableMap,
OrderedSet as ImmutableOrderedSet,
Record as ImmutableRecord,
} from 'immutable';
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { import {
@ -44,95 +40,123 @@ import {
FAVOURITES_EXPAND_SUCCESS, FAVOURITES_EXPAND_SUCCESS,
DISLIKES_FETCH_SUCCESS, DISLIKES_FETCH_SUCCESS,
REACTIONS_FETCH_SUCCESS, REACTIONS_FETCH_SUCCESS,
InteractionsAction,
} from 'pl-fe/actions/interactions'; } from 'pl-fe/actions/interactions';
import { NOTIFICATIONS_UPDATE } from 'pl-fe/actions/notifications'; import { NOTIFICATIONS_UPDATE } from 'pl-fe/actions/notifications';
import type { Account, Notification, PaginatedResponse } from 'pl-api'; import type { Account, EmojiReaction, Notification, PaginatedResponse } from 'pl-api';
import type { APIEntity } from 'pl-fe/types/entities'; import type { APIEntity } from 'pl-fe/types/entities';
const ListRecord = ImmutableRecord({ interface List {
next: null as (() => Promise<PaginatedResponse<Account>>) | null, next: (() => Promise<PaginatedResponse<Account>>) | null;
items: ImmutableOrderedSet<string>(), items: Array<string>;
isLoading: false, isLoading: boolean;
}); }
const ReactionRecord = ImmutableRecord({ interface Reaction {
accounts: ImmutableOrderedSet<string>(), accounts: Array<string>;
count: 0, count: number;
name: '', name: string;
url: null as string | null, url: string | null;
}); }
const ReactionListRecord = ImmutableRecord({ interface ReactionList {
next: null as (() => Promise<PaginatedResponse<Reaction>>) | null, next: (() => Promise<PaginatedResponse<Reaction>>) | null;
items: ImmutableOrderedSet<Reaction>(), items: Array<Reaction>;
isLoading: false, isLoading: boolean;
}); }
const ParticipationRequestRecord = ImmutableRecord({ interface ParticipationRequest {
account: '', account: string;
participation_message: null as string | null, participation_message: string | null;
}); }
const ParticipationRequestListRecord = ImmutableRecord({ interface ParticipationRequestList {
next: null as (() => Promise<PaginatedResponse<any>>) | null, next: (() => Promise<PaginatedResponse<any>>) | null;
items: ImmutableOrderedSet<ParticipationRequest>(), items: Array<ParticipationRequest>;
isLoading: false, isLoading: boolean;
}); }
const ReducerRecord = ImmutableRecord({ type ListKey = 'follow_requests' | 'directory';
followers: ImmutableMap<string, List>(), type NestedListKey = 'reblogged_by' | 'favourited_by' | 'disliked_by' | 'pinned' | 'birthday_reminders' | 'familiar_followers' | 'event_participations' | 'membership_requests' | 'group_blocks';
following: ImmutableMap<string, List>(),
reblogged_by: ImmutableMap<string, List>(),
favourited_by: ImmutableMap<string, List>(),
disliked_by: ImmutableMap<string, List>(),
reactions: ImmutableMap<string, ReactionList>(),
follow_requests: ListRecord(),
mutes: ListRecord(),
directory: ListRecord({ isLoading: true }),
pinned: ImmutableMap<string, List>(),
birthday_reminders: ImmutableMap<string, List>(),
familiar_followers: ImmutableMap<string, List>(),
event_participations: ImmutableMap<string, List>(),
event_participation_requests: ImmutableMap<string, ParticipationRequestList>(),
membership_requests: ImmutableMap<string, List>(),
group_blocks: ImmutableMap<string, List>(),
});
type State = ReturnType<typeof ReducerRecord>; type State = Record<ListKey, List> & Record<NestedListKey, Record<string, List>> & {
type List = ReturnType<typeof ListRecord>; reactions: Record<string, ReactionList>;
type Reaction = ReturnType<typeof ReactionRecord>; event_participation_requests: Record<string, ParticipationRequestList>;
type ReactionList = ReturnType<typeof ReactionListRecord>; };
type ParticipationRequest = ReturnType<typeof ParticipationRequestRecord>;
type ParticipationRequestList = ReturnType<typeof ParticipationRequestListRecord>;
type Items = ImmutableOrderedSet<string>;
type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'disliked_by' | 'reactions' | 'pinned' | 'birthday_reminders' | 'familiar_followers' | 'event_participations' | 'event_participation_requests' | 'membership_requests' | 'group_blocks', string];
type ListPath = ['follow_requests' | 'mutes' | 'directory'];
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next?: (() => any) | null) => const initialState: State = {
state.setIn(path, ListRecord({ reblogged_by: {},
next, favourited_by: {},
items: ImmutableOrderedSet(accounts.map(item => item.id)), disliked_by: {},
})); reactions: {},
follow_requests: { next: null, items: [], isLoading: false },
directory: { next: null, items: [], isLoading: true },
pinned: {},
birthday_reminders: {},
familiar_followers: {},
event_participations: {},
event_participation_requests: {},
membership_requests: {},
group_blocks: {},
};
const appendToList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next: (() => any) | null) => type NestedListPath = [NestedListKey, string];
state.updateIn(path, map => (map as List) type ListPath = [ListKey];
.set('next', next)
.set('isLoading', false) const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next: (() => Promise<PaginatedResponse<any>>) | null = null) =>
.update('items', list => (list as Items).concat(accounts.map(item => item.id))), create(state, (draft) => {
); let list: List;
if (path.length === 1) {
list = draft[path[0]];
} else {
list = draft[path[0]][path[1]];
}
const newList = { ...list, next, items: accounts.map(item => item.id), isLoading: false };
if (path.length === 1) {
draft[path[0]] = newList;
} else {
draft[path[0]][path[1]] = newList;
}
});
const appendToList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next: (() => any) | null = null) =>
create(state, (draft) => {
let list: List;
if (path.length === 1) {
list = draft[path[0]];
} else {
list = draft[path[0]][path[1]];
}
list.next = next;
list.isLoading = false;
list.items = [...new Set([...list.items, ...accounts.map(item => item.id)])];
});
const removeFromList = (state: State, path: NestedListPath | ListPath, accountId: string) => const removeFromList = (state: State, path: NestedListPath | ListPath, accountId: string) =>
state.updateIn(path, map => create(state, (draft) => {
(map as List).update('items', list => (list as Items).filterNot(item => item === accountId)), let list: List;
);
if (path.length === 1) {
list = draft[path[0]];
} else {
list = draft[path[0]][path[1]];
}
list.items = list.items.filter(item => item !== accountId);
});
const normalizeFollowRequest = (state: State, notification: Notification) => const normalizeFollowRequest = (state: State, notification: Notification) =>
state.updateIn(['follow_requests', 'items'], list => create(state, (draft) => {
ImmutableOrderedSet([notification.account.id]).union(list as Items), draft.follow_requests.items = [...new Set([notification.account.id, ...draft.follow_requests.items])];
); });
const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction) => { const userLists = (state = initialState, action: DirectoryAction | InteractionsAction | AnyAction): State => {
switch (action.type) { switch (action.type) {
case REBLOGS_FETCH_SUCCESS: case REBLOGS_FETCH_SUCCESS:
return normalizeList(state, ['reblogged_by', action.statusId], action.accounts, action.next); return normalizeList(state, ['reblogged_by', action.statusId], action.accounts, action.next);
@ -145,12 +169,13 @@ const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction)
case DISLIKES_FETCH_SUCCESS: case DISLIKES_FETCH_SUCCESS:
return normalizeList(state, ['disliked_by', action.statusId], action.accounts); return normalizeList(state, ['disliked_by', action.statusId], action.accounts);
case REACTIONS_FETCH_SUCCESS: case REACTIONS_FETCH_SUCCESS:
return state.setIn(['reactions', action.statusId], ReactionListRecord({ return create(state, (draft) => {
items: ImmutableOrderedSet<Reaction>(action.reactions.map(({ accounts, ...reaction }: APIEntity) => ReactionRecord({ draft.reactions[action.statusId] = {
...reaction, items: action.reactions.map((reaction: EmojiReaction) => ({ ...reaction, accounts: reaction.accounts.map(({ id }) => id) })),
accounts: ImmutableOrderedSet(accounts.map((account: APIEntity) => account.id)), next: null,
}))), isLoading: false,
})); };
});
case NOTIFICATIONS_UPDATE: case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state; return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS: case FOLLOW_REQUESTS_FETCH_SUCCESS:
@ -166,10 +191,14 @@ const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction)
return appendToList(state, ['directory'], action.accounts, null); return appendToList(state, ['directory'], action.accounts, null);
case DIRECTORY_FETCH_REQUEST: case DIRECTORY_FETCH_REQUEST:
case DIRECTORY_EXPAND_REQUEST: case DIRECTORY_EXPAND_REQUEST:
return state.setIn(['directory', 'isLoading'], true); return create(state, (draft) => {
draft.directory.isLoading = true;
});
case DIRECTORY_FETCH_FAIL: case DIRECTORY_FETCH_FAIL:
case DIRECTORY_EXPAND_FAIL: case DIRECTORY_EXPAND_FAIL:
return state.setIn(['directory', 'isLoading'], false); return create(state, (draft) => {
draft.directory.isLoading = false;
});
case PINNED_ACCOUNTS_FETCH_SUCCESS: case PINNED_ACCOUNTS_FETCH_SUCCESS:
return normalizeList(state, ['pinned', action.accountId], action.accounts, action.next); return normalizeList(state, ['pinned', action.accountId], action.accounts, action.next);
case BIRTHDAY_REMINDERS_FETCH_SUCCESS: case BIRTHDAY_REMINDERS_FETCH_SUCCESS:
@ -181,43 +210,60 @@ const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction)
case EVENT_PARTICIPATIONS_EXPAND_SUCCESS: case EVENT_PARTICIPATIONS_EXPAND_SUCCESS:
return appendToList(state, ['event_participations', action.statusId], action.accounts, action.next); return appendToList(state, ['event_participations', action.statusId], action.accounts, action.next);
case EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS: case EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS:
return state.setIn(['event_participation_requests', action.statusId], ParticipationRequestListRecord({ return create(state, (draft) => {
next: action.next, draft.event_participation_requests[action.statusId] = {
items: ImmutableOrderedSet(action.participations.map(({ account, participation_message }: APIEntity) => ParticipationRequestRecord({ next: action.next,
account: account.id, items: action.participations.map(({ account, participation_message }: APIEntity) => ({
participation_message,
}))),
}));
case EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS:
return state.updateIn(
['event_participation_requests', action.statusId, 'items'],
(items) => (items as ImmutableOrderedSet<ParticipationRequest>)
.union(action.participations.map(({ account, participation_message }: APIEntity) => ParticipationRequestRecord({
account: account.id, account: account.id,
participation_message, participation_message,
}))), })),
); isLoading: false,
};
});
case EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS:
return create(state, (draft) => {
const list = draft.event_participation_requests[action.statusId];
list.next = action.next;
list.items = [...list.items, ...action.participations.map(({ account, participation_message }: APIEntity) => ({
account: account.id,
participation_message,
}))];
list.isLoading = false;
});
case EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS: case EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS:
case EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS: case EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS:
return state.updateIn( return create(state, (draft) => {
['event_participation_requests', action.statusId, 'items'], const list = draft.event_participation_requests[action.statusId];
items => (items as ImmutableOrderedSet<ParticipationRequest>).filter(({ account }) => account !== action.accountId), if (list.items) list.items = list.items.filter(item => item !== action.accountId);
); });
case GROUP_BLOCKS_FETCH_SUCCESS: case GROUP_BLOCKS_FETCH_SUCCESS:
return normalizeList(state, ['group_blocks', action.groupId], action.accounts, action.next); return normalizeList(state, ['group_blocks', action.groupId], action.accounts, action.next);
case GROUP_BLOCKS_FETCH_REQUEST: case GROUP_BLOCKS_FETCH_REQUEST:
return state.setIn(['group_blocks', action.groupId, 'isLoading'], true); return create(state, (draft) => {
draft.group_blocks[action.groupId] = {
items: [],
next: null,
isLoading: true,
};
});
case GROUP_BLOCKS_FETCH_FAIL: case GROUP_BLOCKS_FETCH_FAIL:
return state.setIn(['group_blocks', action.groupId, 'isLoading'], false); return create(state, (draft) => {
draft.group_blocks[action.groupId] = {
items: [],
next: null,
isLoading: false,
};
});
case GROUP_UNBLOCK_SUCCESS: case GROUP_UNBLOCK_SUCCESS:
return state.updateIn(['group_blocks', action.groupId, 'items'], list => (list as ImmutableOrderedSet<string>).filterNot(item => item === action.accountId)); return create(state, (draft) => {
const list = draft.group_blocks[action.groupId];
if (list.items) list.items = list.items.filter(item => item !== action.accountId);
});
default: default:
return state; return state;
} }
}; };
export { export {
ListRecord,
ReducerRecord,
userLists as default, userLists as default,
}; };

View file

@ -51,6 +51,8 @@ const makeGetAccount = () => createSelector([
}; };
}); });
type SelectedAccount = Exclude<ReturnType<ReturnType<typeof makeGetAccount>>, null>;
const toServerSideType = (columnType: string): Filter['context'][0] => { const toServerSideType = (columnType: string): Filter['context'][0] => {
switch (columnType) { switch (columnType) {
case 'home': case 'home':
@ -277,11 +279,12 @@ const makeGetOtherAccounts = () => createSelector([
getAuthUserIds, getAuthUserIds,
(state: RootState) => state.me, (state: RootState) => state.me,
], (accounts, authUserIds, me) => ], (accounts, authUserIds, me) =>
authUserIds.reduce((list: ImmutableList<any>, id: string) => { authUserIds.reduce<Array<Account>>((list, id) => {
if (id === me) return list; if (id === me) return list;
const account = accounts?.[id]; const account = accounts?.[id];
return account ? list.push(account) : list; if (account) list.push(account);
}, ImmutableList()), return list;
}, []),
); );
const getSimplePolicy = createSelector([ const getSimplePolicy = createSelector([
@ -353,8 +356,10 @@ const makeGetStatusIds = () => createSelector([
export { export {
type RemoteInstance, type RemoteInstance,
selectAccount, selectAccount,
selectAccounts,
selectOwnAccount, selectOwnAccount,
makeGetAccount, makeGetAccount,
type SelectedAccount,
getFilters, getFilters,
regexFromFilters, regexFromFilters,
makeGetStatus, makeGetStatus,