Files
ncd-fe/packages/pl-fe/src/reducers/user-lists.ts
marcin mikołajczak 0811af7ad7 pl-fe: Move user lists away from immutable
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-11-06 12:56:09 +01:00

270 lines
9.4 KiB
TypeScript

import { create } from 'mutative';
import { AnyAction } from 'redux';
import {
FOLLOW_REQUESTS_FETCH_SUCCESS,
FOLLOW_REQUESTS_EXPAND_SUCCESS,
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
FOLLOW_REQUEST_REJECT_SUCCESS,
PINNED_ACCOUNTS_FETCH_SUCCESS,
BIRTHDAY_REMINDERS_FETCH_SUCCESS,
} from 'pl-fe/actions/accounts';
import {
DIRECTORY_FETCH_REQUEST,
DIRECTORY_FETCH_SUCCESS,
DIRECTORY_FETCH_FAIL,
DIRECTORY_EXPAND_REQUEST,
DIRECTORY_EXPAND_SUCCESS,
DIRECTORY_EXPAND_FAIL,
DirectoryAction,
} from 'pl-fe/actions/directory';
import {
EVENT_PARTICIPATIONS_EXPAND_SUCCESS,
EVENT_PARTICIPATIONS_FETCH_SUCCESS,
EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS,
EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS,
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS,
EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS,
} from 'pl-fe/actions/events';
import { FAMILIAR_FOLLOWERS_FETCH_SUCCESS } from 'pl-fe/actions/familiar-followers';
import {
GROUP_BLOCKS_FETCH_REQUEST,
GROUP_BLOCKS_FETCH_SUCCESS,
GROUP_BLOCKS_FETCH_FAIL,
GROUP_UNBLOCK_SUCCESS,
} from 'pl-fe/actions/groups';
import {
REBLOGS_FETCH_SUCCESS,
REBLOGS_EXPAND_SUCCESS,
FAVOURITES_FETCH_SUCCESS,
FAVOURITES_EXPAND_SUCCESS,
DISLIKES_FETCH_SUCCESS,
REACTIONS_FETCH_SUCCESS,
InteractionsAction,
} from 'pl-fe/actions/interactions';
import { NOTIFICATIONS_UPDATE } from 'pl-fe/actions/notifications';
import type { Account, EmojiReaction, Notification, PaginatedResponse } from 'pl-api';
import type { APIEntity } from 'pl-fe/types/entities';
interface List {
next: (() => Promise<PaginatedResponse<Account>>) | null;
items: Array<string>;
isLoading: boolean;
}
interface Reaction {
accounts: Array<string>;
count: number;
name: string;
url: string | null;
}
interface ReactionList {
next: (() => Promise<PaginatedResponse<Reaction>>) | null;
items: Array<Reaction>;
isLoading: boolean;
}
interface ParticipationRequest {
account: string;
participation_message: string | null;
}
interface ParticipationRequestList {
next: (() => Promise<PaginatedResponse<any>>) | null;
items: Array<ParticipationRequest>;
isLoading: boolean;
}
type ListKey = 'follow_requests' | 'directory';
type NestedListKey = 'reblogged_by' | 'favourited_by' | 'disliked_by' | 'pinned' | 'birthday_reminders' | 'familiar_followers' | 'event_participations' | 'membership_requests' | 'group_blocks';
type State = Record<ListKey, List> & Record<NestedListKey, Record<string, List>> & {
reactions: Record<string, ReactionList>;
event_participation_requests: Record<string, ParticipationRequestList>;
};
const initialState: State = {
reblogged_by: {},
favourited_by: {},
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: {},
};
type NestedListPath = [NestedListKey, string];
type ListPath = [ListKey];
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next: (() => Promise<PaginatedResponse<any>>) | null = null) =>
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) =>
create(state, (draft) => {
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) =>
create(state, (draft) => {
draft.follow_requests.items = [...new Set([notification.account.id, ...draft.follow_requests.items])];
});
const userLists = (state = initialState, action: DirectoryAction | InteractionsAction | AnyAction): State => {
switch (action.type) {
case REBLOGS_FETCH_SUCCESS:
return normalizeList(state, ['reblogged_by', action.statusId], action.accounts, action.next);
case REBLOGS_EXPAND_SUCCESS:
return appendToList(state, ['reblogged_by', action.statusId], action.accounts, action.next);
case FAVOURITES_FETCH_SUCCESS:
return normalizeList(state, ['favourited_by', action.statusId], action.accounts, action.next);
case FAVOURITES_EXPAND_SUCCESS:
return appendToList(state, ['favourited_by', action.statusId], action.accounts, action.next);
case DISLIKES_FETCH_SUCCESS:
return normalizeList(state, ['disliked_by', action.statusId], action.accounts);
case REACTIONS_FETCH_SUCCESS:
return create(state, (draft) => {
draft.reactions[action.statusId] = {
items: action.reactions.map((reaction: EmojiReaction) => ({ ...reaction, accounts: reaction.accounts.map(({ id }) => id) })),
next: null,
isLoading: false,
};
});
case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS:
return normalizeList(state, ['follow_requests'], action.accounts, action.next);
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
return appendToList(state, ['follow_requests'], action.accounts, action.next);
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
case FOLLOW_REQUEST_REJECT_SUCCESS:
return removeFromList(state, ['follow_requests'], action.accountId);
case DIRECTORY_FETCH_SUCCESS:
return normalizeList(state, ['directory'], action.accounts);
case DIRECTORY_EXPAND_SUCCESS:
return appendToList(state, ['directory'], action.accounts, null);
case DIRECTORY_FETCH_REQUEST:
case DIRECTORY_EXPAND_REQUEST:
return create(state, (draft) => {
draft.directory.isLoading = true;
});
case DIRECTORY_FETCH_FAIL:
case DIRECTORY_EXPAND_FAIL:
return create(state, (draft) => {
draft.directory.isLoading = false;
});
case PINNED_ACCOUNTS_FETCH_SUCCESS:
return normalizeList(state, ['pinned', action.accountId], action.accounts, action.next);
case BIRTHDAY_REMINDERS_FETCH_SUCCESS:
return normalizeList(state, ['birthday_reminders', action.accountId], action.accounts, action.next);
case FAMILIAR_FOLLOWERS_FETCH_SUCCESS:
return normalizeList(state, ['familiar_followers', action.accountId], action.accounts, action.next);
case EVENT_PARTICIPATIONS_FETCH_SUCCESS:
return normalizeList(state, ['event_participations', action.statusId], action.accounts, action.next);
case EVENT_PARTICIPATIONS_EXPAND_SUCCESS:
return appendToList(state, ['event_participations', action.statusId], action.accounts, action.next);
case EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS:
return create(state, (draft) => {
draft.event_participation_requests[action.statusId] = {
next: action.next,
items: action.participations.map(({ account, participation_message }: APIEntity) => ({
account: account.id,
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_REJECT_SUCCESS:
return create(state, (draft) => {
const list = draft.event_participation_requests[action.statusId];
if (list.items) list.items = list.items.filter(item => item !== action.accountId);
});
case GROUP_BLOCKS_FETCH_SUCCESS:
return normalizeList(state, ['group_blocks', action.groupId], action.accounts, action.next);
case GROUP_BLOCKS_FETCH_REQUEST:
return create(state, (draft) => {
draft.group_blocks[action.groupId] = {
items: [],
next: null,
isLoading: true,
};
});
case GROUP_BLOCKS_FETCH_FAIL:
return create(state, (draft) => {
draft.group_blocks[action.groupId] = {
items: [],
next: null,
isLoading: false,
};
});
case GROUP_UNBLOCK_SUCCESS:
return create(state, (draft) => {
const list = draft.group_blocks[action.groupId];
if (list.items) list.items = list.items.filter(item => item !== action.accountId);
});
default:
return state;
}
};
export {
userLists as default,
};