frontend-rw #1

Merged
marcin merged 347 commits from frontend-rw into develop 2024-12-05 15:32:18 -08:00
41 changed files with 482 additions and 463 deletions
Showing only changes of commit 563e5288fb - Show all commits

View file

@ -255,7 +255,7 @@ class PlApiClient {
unlisten: (listener: any) => void; unlisten: (listener: any) => void;
subscribe: (stream: string, params?: { list?: string; tag?: string }) => void; subscribe: (stream: string, params?: { list?: string; tag?: string }) => void;
unsubscribe: (stream: string, params?: { list?: string; tag?: string }) => void; unsubscribe: (stream: string, params?: { list?: string; tag?: string }) => void;
close: () => void; close: () => void;
}; };
constructor(baseURL: string, accessToken?: string, { constructor(baseURL: string, accessToken?: string, {

View file

@ -8,7 +8,6 @@ import { getClient, type PlfeResponse } from '../api';
import { importEntities } from './importer'; import { importEntities } from './importer';
import type { Map as ImmutableMap } from 'immutable';
import type { MinifiedStatus } from 'pl-fe/reducers/statuses'; import type { MinifiedStatus } from 'pl-fe/reducers/statuses';
import type { AppDispatch, RootState } from 'pl-fe/store'; import type { AppDispatch, RootState } from 'pl-fe/store';
import type { History } from 'pl-fe/types/history'; import type { History } from 'pl-fe/types/history';
@ -190,7 +189,7 @@ const blockAccountRequest = (accountId: string) => ({
accountId, accountId,
}); });
const blockAccountSuccess = (relationship: Relationship, statuses: ImmutableMap<string, MinifiedStatus>) => ({ const blockAccountSuccess = (relationship: Relationship, statuses: Record<string, MinifiedStatus>) => ({
type: ACCOUNT_BLOCK_SUCCESS, type: ACCOUNT_BLOCK_SUCCESS,
relationship, relationship,
statuses, statuses,
@ -245,7 +244,7 @@ const muteAccountRequest = (accountId: string) => ({
accountId, accountId,
}); });
const muteAccountSuccess = (relationship: Relationship, statuses: ImmutableMap<string, MinifiedStatus>) => ({ const muteAccountSuccess = (relationship: Relationship, statuses: Record<string, MinifiedStatus>) => ({
type: ACCOUNT_MUTE_SUCCESS, type: ACCOUNT_MUTE_SUCCESS,
relationship, relationship,
statuses, statuses,

View file

@ -162,7 +162,7 @@ const submitEventFail = (error: unknown) => ({
const joinEvent = (statusId: string, participationMessage?: string) => const joinEvent = (statusId: string, participationMessage?: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses.get(statusId); const status = getState().statuses[statusId];
if (!status || !status.event || status.event.join_state) { if (!status || !status.event || status.event.join_state) {
return dispatch(noOp); return dispatch(noOp);
@ -204,7 +204,7 @@ const joinEventFail = (error: unknown, statusId: string, previousState: string |
const leaveEvent = (statusId: string) => const leaveEvent = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses.get(statusId); const status = getState().statuses[statusId];
if (!status || !status.event || !status.event.join_state) { if (!status || !status.event || !status.event.join_state) {
return dispatch(noOp); return dispatch(noOp);

View file

@ -114,7 +114,7 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) => const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const acct = state.statuses.get(statusId)!.account.acct; const acct = state.statuses[statusId]!.account.acct;
useModalsStore.getState().openModal('CONFIRM', { useModalsStore.getState().openModal('CONFIRM', {
heading: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading), heading: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading),
@ -133,7 +133,7 @@ const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensiti
const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) => const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const acct = state.statuses.get(statusId)!.account.acct; const acct = state.statuses[statusId]!.account.acct;
useModalsStore.getState().openModal('CONFIRM', { useModalsStore.getState().openModal('CONFIRM', {
heading: intl.formatMessage(messages.deleteStatusHeading), heading: intl.formatMessage(messages.deleteStatusHeading),

View file

@ -26,7 +26,7 @@ const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions:
const updateMrf = (host: string, restrictions: Record<string, any>) => const updateMrf = (host: string, restrictions: Record<string, any>) =>
(dispatch: AppDispatch, getState: () => RootState) => (dispatch: AppDispatch, getState: () => RootState) =>
dispatch(fetchConfig()).then(() => { dispatch(fetchConfig()).then(() => {
const configs = getState().admin.get('configs'); const configs = getState().admin.configs;
const simplePolicy = ConfigDB.toSimplePolicy(configs); const simplePolicy = ConfigDB.toSimplePolicy(configs);
const merged = simplePolicyMerge(simplePolicy, host, restrictions); const merged = simplePolicyMerge(simplePolicy, host, restrictions);
const config = ConfigDB.fromSimplePolicy(merged); const config = ConfigDB.fromSimplePolicy(merged);

View file

@ -17,8 +17,7 @@ const PLFE_CONFIG_REMEMBER_SUCCESS = 'PLFE_CONFIG_REMEMBER_SUCCESS' as const;
const getPlFeConfig = createSelector([ const getPlFeConfig = createSelector([
(state: RootState) => state.plfe, (state: RootState) => state.plfe,
(state: RootState) => state.auth.client.features, ], (plfe) => {
], (plfe, features) => {
// Do some additional normalization with the state // Do some additional normalization with the state
return normalizePlFeConfig(plfe); return normalizePlFeConfig(plfe);
}); });

View file

@ -96,7 +96,7 @@ const createStatus = (params: CreateStatusParams, idempotencyKey: string, status
const editStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const editStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const status = state.statuses.get(statusId)!; const status = state.statuses[statusId]!;
const poll = status.poll_id ? state.polls.get(status.poll_id) : undefined; const poll = status.poll_id ? state.polls.get(status.poll_id) : undefined;
dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
@ -133,7 +133,7 @@ const deleteStatus = (statusId: string, withRedraft = false) =>
const state = getState(); const state = getState();
const status = state.statuses.get(statusId)!; const status = state.statuses[statusId]!;
const poll = status.poll_id ? state.polls.get(status.poll_id) : undefined; const poll = status.poll_id ? state.polls.get(status.poll_id) : undefined;
dispatch({ type: STATUS_DELETE_REQUEST, params: status }); dispatch({ type: STATUS_DELETE_REQUEST, params: status });

View file

@ -1,5 +1,3 @@
import { Map as ImmutableMap } from 'immutable';
import { getLocale } from 'pl-fe/actions/settings'; import { getLocale } from 'pl-fe/actions/settings';
import { useSettingsStore } from 'pl-fe/stores/settings'; import { useSettingsStore } from 'pl-fe/stores/settings';
import { shouldFilter } from 'pl-fe/utils/timelines'; import { shouldFilter } from 'pl-fe/utils/timelines';
@ -106,15 +104,17 @@ interface TimelineDeleteAction {
type: typeof TIMELINE_DELETE; type: typeof TIMELINE_DELETE;
statusId: string; statusId: string;
accountId: string; accountId: string;
references: ImmutableMap<string, readonly [statusId: string, accountId: string]>; references: Record<string, readonly [statusId: string, accountId: string]>;
reblogOf: string | null; reblogOf: string | null;
} }
const deleteFromTimelines = (statusId: string) => const deleteFromTimelines = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const accountId = getState().statuses.get(statusId)?.account?.id!; const accountId = getState().statuses[statusId]?.account?.id!;
const references = getState().statuses.filter(status => status.reblog_id === statusId).map(status => [status.id, status.account_id] as const); const references = Object.fromEntries(Object.entries(getState().statuses)
const reblogOf = getState().statuses.get(statusId)?.reblog_id || null; .filter(([key, status]) => [key, status.reblog_id === statusId])
.map(([key, status]) => [key, [status.id, status.account_id] as const]));
const reblogOf = getState().statuses[statusId]?.reblog_id || null;
dispatch<TimelineDeleteAction>({ dispatch<TimelineDeleteAction>({
type: TIMELINE_DELETE, type: TIMELINE_DELETE,

View file

@ -12,8 +12,8 @@ FaviconService.initFaviconService();
const getNotifTotals = (state: RootState): number => { const getNotifTotals = (state: RootState): number => {
const notifications = state.notifications.unread || 0; const notifications = state.notifications.unread || 0;
const reports = state.admin.openReports.count(); const reports = state.admin.openReports.length;
const approvals = state.admin.awaitingApproval.count(); const approvals = state.admin.awaitingApproval.length;
return notifications + reports + approvals; return notifications + reports + approvals;
}; };

View file

@ -50,7 +50,7 @@ const SidebarNavigation = () => {
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.count());
const interactionRequestsCount = useInteractionRequestsCount().data || 0; const interactionRequestsCount = useInteractionRequestsCount().data || 0;
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); 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);
const draftCount = useAppSelector((state) => state.draft_statuses.size); const draftCount = useAppSelector((state) => state.draft_statuses.size);

View file

@ -24,7 +24,7 @@ const StatusHoverCard: React.FC<IStatusHoverCard> = ({ visible = true }) => {
const { statusId, ref, closeStatusHoverCard, updateStatusHoverCard } = useStatusHoverCardStore(); const { statusId, ref, closeStatusHoverCard, updateStatusHoverCard } = useStatusHoverCardStore();
const status = useAppSelector(state => state.statuses.get(statusId!)); const status = useAppSelector(state => state.statuses[statusId!]);
useEffect(() => { useEffect(() => {
if (statusId && !status) { if (statusId && !status) {

View file

@ -1,4 +1,3 @@
import { List as ImmutableList } from 'immutable';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@ -27,7 +26,7 @@ const AccountGallery = () => {
isUnavailable, isUnavailable,
} = useAccountLookup(username, { withRelationship: true }); } = useAccountLookup(username, { withRelationship: true });
const attachments: ImmutableList<AccountGalleryAttachment> = useAppSelector((state) => account ? getAccountGallery(state, account.id) : ImmutableList()); const attachments: Array<AccountGalleryAttachment> = useAppSelector((state) => account ? getAccountGallery(state, account.id) : []);
const isLoading = useAppSelector((state) => state.timelines.get(`account:${account?.id}:with_replies:media`)?.isLoading); const isLoading = useAppSelector((state) => state.timelines.get(`account:${account?.id}:with_replies:media`)?.isLoading);
const hasMore = useAppSelector((state) => state.timelines.get(`account:${account?.id}:with_replies:media`)?.hasMore); const hasMore = useAppSelector((state) => state.timelines.get(`account:${account?.id}:with_replies:media`)?.hasMore);
@ -81,7 +80,7 @@ const AccountGallery = () => {
let loadOlder = null; let loadOlder = null;
if (hasMore && !(isLoading && attachments.size === 0)) { if (hasMore && !(isLoading && attachments.length === 0)) {
loadOlder = <LoadMore className='my-auto mt-4' visible={!isLoading} onClick={handleLoadOlder} />; loadOlder = <LoadMore className='my-auto mt-4' visible={!isLoading} onClick={handleLoadOlder} />;
} }
@ -103,11 +102,11 @@ const AccountGallery = () => {
key={`${attachment.status.id}+${attachment.id}`} key={`${attachment.status.id}+${attachment.id}`}
attachment={attachment} attachment={attachment}
onOpenMedia={handleOpenMedia} onOpenMedia={handleOpenMedia}
isLast={index === attachments.size - 1} isLast={index === attachments.length - 1}
/> />
))} ))}
{!isLoading && attachments.size === 0 && ( {!isLoading && attachments.length === 0 && (
<div className='empty-column-indicator col-span-2 sm:col-span-3'> <div className='empty-column-indicator col-span-2 sm:col-span-3'>
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' /> <FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
</div> </div>
@ -116,7 +115,7 @@ const AccountGallery = () => {
{loadOlder} {loadOlder}
{isLoading && attachments.size === 0 && ( {isLoading && attachments.length === 0 && (
<div className='relative flex-auto px-8 py-4'> <div className='relative flex-auto px-8 py-4'>
<Spinner /> <Spinner />
</div> </div>

View file

@ -15,8 +15,8 @@ const AdminTabs: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const match = useRouteMatch(); const match = useRouteMatch();
const approvalCount = useAppSelector(state => state.admin.awaitingApproval.count()); const approvalCount = useAppSelector(state => state.admin.awaitingApproval.length);
const reportsCount = useAppSelector(state => state.admin.openReports.count()); const reportsCount = useAppSelector(state => state.admin.openReports.length);
const tabs = [{ const tabs = [{
name: '/pl-fe/admin', name: '/pl-fe/admin',

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@ -22,9 +21,9 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
const intl = useIntl(); const intl = useIntl();
const history = useHistory(); const history = useHistory();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit)); const accountIds = useAppSelector<Array<string>>((state) => state.admin.latestUsers.slice(0, limit));
const [total, setTotal] = useState<number | undefined>(accountIds.size); const [total, setTotal] = useState<number | undefined>(accountIds.length);
useEffect(() => { useEffect(() => {
dispatch(fetchUsers({ dispatch(fetchUsers({
@ -46,7 +45,7 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
onActionClick={handleAction} onActionClick={handleAction}
actionTitle={intl.formatMessage(messages.expand, { count: total })} actionTitle={intl.formatMessage(messages.expand, { count: total })}
> >
{accountIds.take(limit).map((account) => ( {accountIds.slice(0, limit).map((account) => (
<AccountContainer key={account} id={account} withRelationship={false} withDate /> <AccountContainer key={account} id={account} withRelationship={false} withDate />
))} ))}
</Widget> </Widget>

View file

@ -18,7 +18,7 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { account } = useAccount(accountId); const { account } = useAccount(accountId);
const adminAccount = useAppSelector(state => state.admin.users.get(accountId)); const adminAccount = useAppSelector(state => state.admin.users[accountId]);
if (!account) return null; if (!account) return null;

View file

@ -29,7 +29,7 @@ const AwaitingApproval: React.FC = () => {
.catch(() => {}); .catch(() => {});
}, []); }, []);
const showLoading = isLoading && accountIds.count() === 0; const showLoading = isLoading && accountIds.length === 0;
return ( return (
<ScrollableList <ScrollableList

View file

@ -20,7 +20,7 @@ const Reports: React.FC = () => {
const [isLoading, setLoading] = useState(true); const [isLoading, setLoading] = useState(true);
const reports = useAppSelector(state => state.admin.openReports.toList()); const reports = useAppSelector(state => state.admin.openReports);
useEffect(() => { useEffect(() => {
dispatch(fetchReports()) dispatch(fetchReports())
@ -28,7 +28,7 @@ const Reports: React.FC = () => {
.catch(() => {}); .catch(() => {});
}, []); }, []);
const showLoading = isLoading && reports.count() === 0; const showLoading = isLoading && reports.length === 0;
return ( return (
<ScrollableList <ScrollableList

View file

@ -37,7 +37,7 @@ const messages = defineMessages({
}); });
interface IUploadButton { interface IUploadButton {
onSelectFile: (src: string) => void; onSelectFile: (src: string) => void;
} }
const UploadButton: React.FC<IUploadButton> = ({ onSelectFile }) => { const UploadButton: React.FC<IUploadButton> = ({ onSelectFile }) => {

View file

@ -40,7 +40,7 @@ const StatePlugin: React.FC<IStatePlugin> = ({ composeId, isWysiwyg }) => {
for (const id of ids) { for (const id of ids) {
if (compose?.dismissed_quotes.includes(id)) continue; if (compose?.dismissed_quotes.includes(id)) continue;
if (state.statuses.get(id)) { if (state.statuses[id]) {
quoteId = id; quoteId = id;
break; break;
} }

View file

@ -9,7 +9,7 @@ import { ElementNode, RangeSelection, TextNode } from 'lexical';
export const getSelectedNode = ( export const getSelectedNode = (
selection: RangeSelection, selection: RangeSelection,
): TextNode | ElementNode => { ): TextNode | ElementNode => {
const anchor = selection.anchor; const anchor = selection.anchor;
const focus = selection.focus; const focus = selection.focus;
const anchorNode = selection.anchor.getNode(); const anchorNode = selection.anchor.getNode();

View file

@ -1,13 +1,8 @@
import { fromJS } from 'immutable'; import coinDB from './manifest-map';
import manifestMap from './manifest-map';
// All this does is converts the result from manifest_map.js into an ImmutableMap
const coinDB = fromJS(manifestMap);
/** Get title from CoinDB based on ticker symbol */ /** Get title from CoinDB based on ticker symbol */
const getTitle = (ticker: string): string => { const getTitle = (ticker: string): string => {
const title = coinDB.getIn([ticker, 'name']); const title = coinDB[ticker]?.name;
return typeof title === 'string' ? title : ''; return typeof title === 'string' ? title : '';
}; };

View file

@ -1,4 +1,3 @@
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
@ -38,7 +37,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statu
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
const descendantsIds = useAppSelector(state => getDescendantsIds(state, statusId).delete(statusId)); const descendantsIds = useAppSelector(state => getDescendantsIds(state, statusId).filter(id => id !== statusId));
const [isLoaded, setIsLoaded] = useState<boolean>(!!status); const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
@ -61,12 +60,12 @@ const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statu
}, [isLoaded, me]); }, [isLoaded, me]);
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);
}; };
@ -110,7 +109,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statu
); );
}; };
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 if (id.startsWith('末pending-')) { } else if (id.startsWith('末pending-')) {
@ -120,7 +119,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statu
} }
}); });
const hasDescendants = descendantsIds.size > 0; const hasDescendants = descendantsIds.length > 0;
if (!status && isLoaded) { if (!status && isLoaded) {
return ( return (
@ -135,7 +134,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statu
const children: JSX.Element[] = []; const children: JSX.Element[] = [];
if (hasDescendants) { if (hasDescendants) {
children.push(...renderChildren(descendantsIds).toArray()); children.push(...renderChildren(descendantsIds));
} }
return ( return (

View file

@ -17,8 +17,6 @@ import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
import { makeGetStatus } from 'pl-fe/selectors'; import { makeGetStatus } from 'pl-fe/selectors';
import { useModalsStore } from 'pl-fe/stores/modals'; import { useModalsStore } from 'pl-fe/stores/modals';
import type { Status as StatusEntity } from 'pl-fe/normalizers/status';
type RouteParams = { statusId: string }; type RouteParams = { statusId: string };
interface IEventInformation { interface IEventInformation {
@ -30,7 +28,7 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
const getStatus = useCallback(makeGetStatus(), []); const getStatus = useCallback(makeGetStatus(), []);
const intl = useIntl(); const intl = useIntl();
const status = useAppSelector(state => getStatus(state, { id: params.statusId })) as StatusEntity; const status = useAppSelector(state => getStatus(state, { id: params.statusId }))!;
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { tileServer } = usePlFeConfig(); const { tileServer } = usePlFeConfig();

View file

@ -52,7 +52,7 @@ interface IInteractionRequestStatus {
} }
const InteractionRequestStatus: React.FC<IInteractionRequestStatus> = ({ id: statusId, hasReply, isReply, actions }) => { const InteractionRequestStatus: React.FC<IInteractionRequestStatus> = ({ id: statusId, hasReply, isReply, actions }) => {
const status = useAppSelector((state) => state.statuses.get(statusId)); const status = useAppSelector((state) => state.statuses[statusId]);
if (!status) return null; if (!status) return null;

View file

@ -128,7 +128,8 @@ const PlFeConfig: React.FC = () => {
}; };
const addStreamItem = (path: ConfigPath, template: Template) => () => { const addStreamItem = (path: ConfigPath, template: Template) => () => {
const items = data.getIn(path) || ImmutableList(); let items = data;
path.forEach(key => items = items?.[key] || []);
setConfig(path, items.push(template)); setConfig(path, items.push(template));
}; };
@ -257,7 +258,7 @@ const PlFeConfig: React.FC = () => {
<Input <Input
type='text' type='text'
placeholder='/timeline/local' placeholder='/timeline/local'
value={String(data.get('redirectRootNoLogin', ''))} value={String(data.redirectRootNoLogin || '')}
onChange={handleChange(['redirectRootNoLogin'], (e) => e.target.value)} onChange={handleChange(['redirectRootNoLogin'], (e) => e.target.value)}
/> />
</ListItem> </ListItem>
@ -269,7 +270,7 @@ const PlFeConfig: React.FC = () => {
<Input <Input
type='text' type='text'
placeholder='https://01234abcdef@glitch.tip.tld/5678' placeholder='https://01234abcdef@glitch.tip.tld/5678'
value={String(data.get('sentryDsn', ''))} value={String(data.sentryDsn || '')}
onChange={handleChange(['sentryDsn'], (e) => e.target.value)} onChange={handleChange(['sentryDsn'], (e) => e.target.value)}
/> />
</ListItem> </ListItem>

View file

@ -1,5 +1,4 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React from 'react'; import React from 'react';
import StatusContainer from 'pl-fe/containers/status-container'; import StatusContainer from 'pl-fe/containers/status-container';
@ -18,9 +17,9 @@ interface IThreadStatus {
const ThreadStatus: React.FC<IThreadStatus> = (props): JSX.Element => { const ThreadStatus: React.FC<IThreadStatus> = (props): JSX.Element => {
const { id, focusedStatusId } = props; const { id, focusedStatusId } = props;
const replyToId = useAppSelector(state => state.contexts.inReplyTos.get(id)); const replyToId = useAppSelector(state => state.contexts.inReplyTos[id]);
const replyCount = useAppSelector(state => state.contexts.replies.get(id, ImmutableOrderedSet()).size); const replyCount = useAppSelector(state => (state.contexts.replies[id] || []).length);
const isLoaded = useAppSelector(state => Boolean(state.statuses.get(id))); const isLoaded = useAppSelector(state => Boolean(state.statuses[id]));
const renderConnector = (): JSX.Element | null => { const renderConnector = (): JSX.Element | null => {
const isConnectedTop = replyToId && replyToId !== focusedStatusId; const isConnectedTop = replyToId && replyToId !== focusedStatusId;

View file

@ -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, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@ -35,36 +34,36 @@ const makeGetAncestorsIds = () => 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: Array<string> = [];
let id: string | undefined = statusId; let id: string | undefined = statusId;
while (id && !ancestorsIds.includes(id)) { while (id && !ancestorsIds.includes(id)) {
ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds); ancestorsIds = [id, ...ancestorsIds];
id = inReplyTos.get(id); id = inReplyTos[id];
} }
return ancestorsIds; return [...new Set(ancestorsIds)];
}); });
const makeGetDescendantsIds = () => createSelector([ const makeGetDescendantsIds = () => 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: Array<string> = [];
const ids = [statusId]; const ids = [statusId];
while (ids.length > 0) { while (ids.length > 0) {
const id = ids.shift(); const id = ids.shift();
if (!id) break; if (!id) break;
const replies = contextReplies.get(id); const replies = contextReplies[id];
if (descendantsIds.includes(id)) { if (descendantsIds.includes(id)) {
break; break;
} }
if (statusId !== id) { if (statusId !== id) {
descendantsIds = descendantsIds.union([id]); descendantsIds = [...descendantsIds, id];
} }
if (replies) { if (replies) {
@ -74,7 +73,7 @@ const makeGetDescendantsIds = () => createSelector([
} }
} }
return descendantsIds; return [...new Set(descendantsIds)];
}); });
const makeGetThread = () => { const makeGetThread = () => {
@ -87,8 +86,8 @@ const makeGetThread = () => {
(_, statusId: string) => statusId, (_, statusId: string) => statusId,
], ],
(ancestorsIds, descendantsIds, statusId) => { (ancestorsIds, descendantsIds, statusId) => {
ancestorsIds = ancestorsIds.delete(statusId).subtract(descendantsIds); ancestorsIds = ancestorsIds.filter(id => id !== statusId && !descendantsIds.includes(id));
descendantsIds = descendantsIds.delete(statusId).subtract(ancestorsIds); descendantsIds = descendantsIds.filter(id => id !== statusId && !ancestorsIds.includes(id));
return { return {
ancestorsIds, ancestorsIds,
@ -121,8 +120,8 @@ const Thread: React.FC<IThread> = ({
const { ancestorsIds, descendantsIds } = useAppSelector((state) => getThread(state, status.id)); const { ancestorsIds, descendantsIds } = useAppSelector((state) => getThread(state, status.id));
let initialIndex = ancestorsIds.size; let initialIndex = ancestorsIds.length;
if (isModal && initialIndex !== 0) initialIndex = ancestorsIds.size + 1; if (isModal && initialIndex !== 0) initialIndex = ancestorsIds.length + 1;
const node = useRef<HTMLDivElement>(null); const node = useRef<HTMLDivElement>(null);
const statusRef = useRef<HTMLDivElement>(null); const statusRef = useRef<HTMLDivElement>(null);
@ -213,13 +212,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);
} }
@ -228,13 +227,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);
} }
@ -289,7 +288,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 if (id.startsWith('末pending-')) { } else if (id.startsWith('末pending-')) {
@ -301,8 +300,8 @@ 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', { openModal('COMPARE_HISTORY', {
@ -310,8 +309,8 @@ const Thread: React.FC<IThread> = ({
}); });
}; };
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 };
@ -370,13 +369,13 @@ const Thread: React.FC<IThread> = ({
} }
if (hasAncestors) { if (hasAncestors) {
children.push(...renderChildren(ancestorsIds).toArray()); children.push(...renderChildren(ancestorsIds));
} }
children.push(focusedStatus); children.push(focusedStatus);
if (hasDescendants) { if (hasDescendants) {
children.push(...renderChildren(descendantsIds).toArray()); children.push(...renderChildren(descendantsIds));
} }
return ( return (

View file

@ -25,7 +25,7 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
const loading = useAppSelector(state => state.history.getIn([statusId, 'loading'])); const loading = useAppSelector(state => state.history.getIn([statusId, 'loading']));
const versions = useAppSelector(state => state.history.get(statusId)?.items); const versions = useAppSelector(state => state.history.get(statusId)?.items);
const status = useAppSelector(state => state.statuses.get(statusId)); const status = useAppSelector(state => state.statuses[statusId]);
const onClickClose = () => { const onClickClose = () => {
onClose('COMPARE_HISTORY'); onClose('COMPARE_HISTORY');

View file

@ -15,7 +15,7 @@ interface IStatusCheckBox {
} }
const StatusCheckBox: React.FC<IStatusCheckBox> = ({ id, disabled, checked, toggleStatusReport }) => { const StatusCheckBox: React.FC<IStatusCheckBox> = ({ id, disabled, checked, toggleStatusReport }) => {
const status = useAppSelector((state) => state.statuses.get(id)); const status = useAppSelector((state) => state.statuses[id]);
const onToggle: React.ChangeEventHandler<HTMLInputElement> = (e) => toggleStatusReport(e.target.checked); const onToggle: React.ChangeEventHandler<HTMLInputElement> = (e) => toggleStatusReport(e.target.checked);

View file

@ -44,7 +44,7 @@ const reportSteps = {
}; };
const SelectedStatus = ({ statusId }: { statusId: string }) => { const SelectedStatus = ({ statusId }: { statusId: string }) => {
const status = useAppSelector((state) => state.statuses.get(statusId)); const status = useAppSelector((state) => state.statuses[statusId]);
if (!status) { if (!status) {
return null; return null;

View file

@ -16,7 +16,6 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { makeGetStatus } from 'pl-fe/selectors'; import { makeGetStatus } from 'pl-fe/selectors';
import type { BaseModalProps } from '../modal-root'; import type { BaseModalProps } from '../modal-root';
import type { Status as StatusEntity } from 'pl-fe/normalizers/status';
interface SelectBookmarkFolderModalProps { interface SelectBookmarkFolderModalProps {
statusId: string; statusId: string;
@ -24,7 +23,7 @@ interface SelectBookmarkFolderModalProps {
const SelectBookmarkFolderModal: React.FC<SelectBookmarkFolderModalProps & BaseModalProps> = ({ statusId, onClose }) => { const SelectBookmarkFolderModal: React.FC<SelectBookmarkFolderModalProps & BaseModalProps> = ({ statusId, onClose }) => {
const getStatus = useCallback(makeGetStatus(), []); const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector(state => getStatus(state, { id: statusId })) as StatusEntity; const status = useAppSelector(state => getStatus(state, { id: statusId }))!;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [selectedFolder, setSelectedFolder] = useState(status.bookmark_folder); const [selectedFolder, setSelectedFolder] = useState(status.bookmark_folder);

View file

@ -1,4 +1,3 @@
import { List as ImmutableList } from 'immutable';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -28,7 +27,7 @@ const GroupMediaPanel: React.FC<IGroupMediaPanel> = ({ group }) => {
const isMember = !!group?.relationship?.member; const isMember = !!group?.relationship?.member;
const isPrivate = group?.locked; const isPrivate = group?.locked;
const attachments: ImmutableList<AccountGalleryAttachment> = useAppSelector((state) => group ? getGroupGallery(state, group?.id) : ImmutableList()); const attachments: Array<AccountGalleryAttachment> = useAppSelector((state) => group ? getGroupGallery(state, group?.id) : []);
const handleOpenMedia = (attachment: AccountGalleryAttachment): void => { const handleOpenMedia = (attachment: AccountGalleryAttachment): void => {
if (attachment.type === 'video') { if (attachment.type === 'video') {
@ -55,7 +54,7 @@ const GroupMediaPanel: React.FC<IGroupMediaPanel> = ({ group }) => {
const renderAttachments = () => { const renderAttachments = () => {
const nineAttachments = attachments.slice(0, 9); const nineAttachments = attachments.slice(0, 9);
if (!nineAttachments.isEmpty()) { if (nineAttachments.length) {
return ( return (
<div className='grid grid-cols-3 gap-0.5 overflow-hidden rounded-md'> <div className='grid grid-cols-3 gap-0.5 overflow-hidden rounded-md'>
{nineAttachments.map((attachment, index) => ( {nineAttachments.map((attachment, index) => (
@ -63,7 +62,7 @@ const GroupMediaPanel: React.FC<IGroupMediaPanel> = ({ group }) => {
key={`${attachment.status.id}+${attachment.id}`} key={`${attachment.status.id}+${attachment.id}`}
attachment={attachment} attachment={attachment}
onOpenMedia={handleOpenMedia} onOpenMedia={handleOpenMedia}
isLast={index === nineAttachments.size - 1} isLast={index === nineAttachments.length - 1}
/> />
))} ))}
</div> </div>

View file

@ -1,4 +1,3 @@
import { List as ImmutableList } from 'immutable';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -25,7 +24,7 @@ const ProfileMediaPanel: React.FC<IProfileMediaPanel> = ({ account }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const attachments: ImmutableList<AccountGalleryAttachment> = useAppSelector((state) => account ? getAccountGallery(state, account?.id) : ImmutableList()); const attachments: Array<AccountGalleryAttachment> = useAppSelector((state) => account ? getAccountGallery(state, account?.id) : []);
const handleOpenMedia = (attachment: AccountGalleryAttachment): void => { const handleOpenMedia = (attachment: AccountGalleryAttachment): void => {
if (attachment.type === 'video') { if (attachment.type === 'video') {
@ -53,7 +52,7 @@ const ProfileMediaPanel: React.FC<IProfileMediaPanel> = ({ account }) => {
const publicAttachments = attachments.filter(attachment => attachment.status.visibility === 'public'); const publicAttachments = attachments.filter(attachment => attachment.status.visibility === 'public');
const nineAttachments = publicAttachments.slice(0, 9); const nineAttachments = publicAttachments.slice(0, 9);
if (!nineAttachments.isEmpty()) { if (nineAttachments.length) {
return ( return (
<div className='grid grid-cols-3 gap-0.5 overflow-hidden rounded-md'> <div className='grid grid-cols-3 gap-0.5 overflow-hidden rounded-md'>
{nineAttachments.map((attachment, index) => ( {nineAttachments.map((attachment, index) => (
@ -61,7 +60,7 @@ const ProfileMediaPanel: React.FC<IProfileMediaPanel> = ({ account }) => {
key={`${attachment.status.id}+${attachment.id}`} key={`${attachment.status.id}+${attachment.id}`}
attachment={attachment} attachment={attachment}
onOpenMedia={handleOpenMedia} onOpenMedia={handleOpenMedia}
isLast={index === nineAttachments.size - 1} isLast={index === nineAttachments.length - 1}
/> />
))} ))}
</div> </div>

View file

@ -37,12 +37,12 @@ const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotency
account, account,
content: pendingStatus.status.replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */ content: pendingStatus.status.replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
id: `末pending-${idempotencyKey}`, id: `末pending-${idempotencyKey}`,
in_reply_to_account_id: state.statuses.getIn([inReplyToId, 'account'], null), in_reply_to_account_id: state.statuses[inReplyToId || '']?.account_id || null,
in_reply_to_id: inReplyToId, in_reply_to_id: inReplyToId,
media_attachments: (pendingStatus.media_ids || ImmutableList()).map((id: string) => ({ id })), media_attachments: (pendingStatus.media_ids || ImmutableList()).map((id: string) => ({ id })),
mentions: buildMentions(pendingStatus), mentions: buildMentions(pendingStatus),
poll: buildPoll(pendingStatus), poll: buildPoll(pendingStatus),
quote: pendingStatus.quote_id ? state.statuses.get(pendingStatus.quote_id) : null, quote: pendingStatus.quote_id ? state.statuses[pendingStatus.quote_id] : null,
sensitive: pendingStatus.sensitive, sensitive: pendingStatus.sensitive,
visibility: pendingStatus.visibility, visibility: pendingStatus.visibility,
}; };

View file

@ -1,11 +1,5 @@
import {
Map as ImmutableMap,
List as ImmutableList,
Record as ImmutableRecord,
OrderedSet as ImmutableOrderedSet,
fromJS,
} from 'immutable';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { create } from 'mutative';
import { import {
ADMIN_CONFIG_FETCH_SUCCESS, ADMIN_CONFIG_FETCH_SUCCESS,
@ -23,48 +17,41 @@ import type { AdminAccount, AdminGetAccountsParams, AdminReport as BaseAdminRepo
import type { Config } from 'pl-fe/utils/config-db'; import type { Config } from 'pl-fe/utils/config-db';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({ interface State {
reports: ImmutableMap<string, MinifiedReport>(), reports: Record<string, MinifiedReport>;
openReports: ImmutableOrderedSet<string>(), openReports: Array<string>;
users: ImmutableMap<string, MinifiedUser>(), users: Record<string, MinifiedUser>;
latestUsers: ImmutableOrderedSet<string>(), latestUsers: Array<string>;
awaitingApproval: ImmutableOrderedSet<string>(), awaitingApproval: Array<string>;
configs: ImmutableList<Config>(), configs: Array<Config>;
needsReboot: boolean;
}
const initialState: State = {
reports: {},
openReports: [],
users: {},
latestUsers: [],
awaitingApproval: [],
configs: [],
needsReboot: false, needsReboot: false,
}); };
type State = ReturnType<typeof ReducerRecord>;
// Lol https://javascript.plainenglish.io/typescript-essentials-conditionally-filter-types-488705bfbf56
type FilterConditionally<Source, Condition> = Pick<Source, {[K in keyof Source]: Source[K] extends Condition ? K : never}[keyof Source]>;
type SetKeys = keyof FilterConditionally<State, ImmutableOrderedSet<string>>;
const toIds = (items: any[]) => items.map(item => item.id); const toIds = (items: any[]) => items.map(item => item.id);
const mergeSet = (state: State, key: SetKeys, users: Array<AdminAccount>): State => { const maybeImportUnapproved = (state: State, users: Array<AdminAccount>, params?: AdminGetAccountsParams) => {
const newIds = toIds(users);
return state.update(key, (ids: ImmutableOrderedSet<string>) => ids.union(newIds));
};
const replaceSet = (state: State, key: SetKeys, users: Array<AdminAccount>): State => {
const newIds = toIds(users);
return state.set(key, ImmutableOrderedSet(newIds));
};
const maybeImportUnapproved = (state: State, users: Array<AdminAccount>, params?: AdminGetAccountsParams): State => {
if (params?.origin === 'local' && params.status === 'pending') { if (params?.origin === 'local' && params.status === 'pending') {
return mergeSet(state, 'awaitingApproval', users); const newIds = toIds(users);
state.awaitingApproval = [...new Set([...state.awaitingApproval, ...newIds])];
} else { } else {
return state; return state;
} }
}; };
const maybeImportLatest = (state: State, users: Array<AdminAccount>, params?: AdminGetAccountsParams): State => { const maybeImportLatest = (state: State, users: Array<AdminAccount>, params?: AdminGetAccountsParams) => {
if (params?.origin === 'local' && params.status === 'active') { if (params?.origin === 'local' && params.status === 'active') {
return replaceSet(state, 'latestUsers', users); const newIds = toIds(users);
} else { state.latestUsers = newIds;
return state;
} }
}; };
@ -72,28 +59,26 @@ const minifyUser = (user: AdminAccount) => omit(user, ['account']);
type MinifiedUser = ReturnType<typeof minifyUser>; type MinifiedUser = ReturnType<typeof minifyUser>;
const importUsers = (state: State, users: Array<AdminAccount>, params: AdminGetAccountsParams): State => const importUsers = (state: State, users: Array<AdminAccount>, params: AdminGetAccountsParams) => {
state.withMutations(state => { maybeImportUnapproved(state, users, params);
maybeImportUnapproved(state, users, params); maybeImportLatest(state, users, params);
maybeImportLatest(state, users, params);
users.forEach(user => { users.forEach(user => {
const normalizedUser = minifyUser(user);
state.setIn(['users', user.id], normalizedUser);
});
});
const deleteUser = (state: State, accountId: string): State =>
state
.update('awaitingApproval', orderedSet => orderedSet.delete(accountId))
.deleteIn(['users', accountId]);
const approveUser = (state: State, user: AdminAccount): State =>
state.withMutations(state => {
const normalizedUser = minifyUser(user); const normalizedUser = minifyUser(user);
state.update('awaitingApproval', orderedSet => orderedSet.delete(user.id)); state.users[user.id] = normalizedUser;
state.setIn(['users', user.id], normalizedUser);
}); });
};
const deleteUser = (state: State, accountId: string) => {
state.awaitingApproval = state.awaitingApproval.filter(id => id !== accountId);
delete state.users[accountId];
};
const approveUser = (state: State, user: AdminAccount) => {
const normalizedUser = minifyUser(user);
state.awaitingApproval = state.awaitingApproval.filter(id => id !== user.id);
state.users[user.id] = normalizedUser;
};
const minifyReport = (report: AdminReport) => omit( const minifyReport = (report: AdminReport) => omit(
report, report,
@ -102,53 +87,51 @@ const minifyReport = (report: AdminReport) => omit(
type MinifiedReport = ReturnType<typeof minifyReport>; type MinifiedReport = ReturnType<typeof minifyReport>;
const importReports = (state: State, reports: Array<BaseAdminReport>): State => const importReports = (state: State, reports: Array<BaseAdminReport>) => {
state.withMutations(state => { reports.forEach(report => {
reports.forEach(report => { const minifiedReport = minifyReport(normalizeAdminReport(report));
const minifiedReport = minifyReport(normalizeAdminReport(report)); if (!minifiedReport.action_taken) {
if (!minifiedReport.action_taken) { state.openReports = [...new Set([...state.openReports, report.id])];
state.update('openReports', orderedSet => orderedSet.add(report.id)); }
} state.reports[report.id] = minifiedReport;
state.setIn(['reports', report.id], minifiedReport);
});
}); });
};
const handleReportDiffs = (state: State, report: MinifiedReport) => const handleReportDiffs = (state: State, report: MinifiedReport) => {
// Note: the reports here aren't full report objects // Note: the reports here aren't full report objects
// hence the need for a new function. // hence the need for a new function.
state.withMutations(state => { switch (report.action_taken) {
switch (report.action_taken) { case false:
case false: state.openReports = [...new Set([...state.openReports, report.id])];
state.update('openReports', orderedSet => orderedSet.add(report.id)); break;
break; default:
default: state.openReports = state.openReports.filter(id => id !== report.id);
state.update('openReports', orderedSet => orderedSet.delete(report.id)); }
} };
});
const normalizeConfig = (config: any): Config => ImmutableMap(fromJS(config)); const importConfigs = (state: State, configs: any) => {
state.configs = configs;
};
const normalizeConfigs = (configs: any): ImmutableList<Config> => ImmutableList(fromJS(configs)).map(normalizeConfig); const admin = (state = initialState, action: AnyAction): State => {
const importConfigs = (state: State, configs: any): State => state.set('configs', normalizeConfigs(configs));
const admin = (state: State = ReducerRecord(), action: AnyAction): State => {
switch (action.type) { switch (action.type) {
case ADMIN_CONFIG_FETCH_SUCCESS: case ADMIN_CONFIG_FETCH_SUCCESS:
case ADMIN_CONFIG_UPDATE_SUCCESS: case ADMIN_CONFIG_UPDATE_SUCCESS:
return importConfigs(state, action.configs); return create(state, (draft) => importConfigs(draft, action.configs));
case ADMIN_REPORTS_FETCH_SUCCESS: case ADMIN_REPORTS_FETCH_SUCCESS:
return importReports(state, action.reports); return create(state, (draft) => importReports(draft, action.reports));
case ADMIN_REPORT_PATCH_SUCCESS: case ADMIN_REPORT_PATCH_SUCCESS:
return handleReportDiffs(state, action.report); return create(state, (draft) => handleReportDiffs(draft, action.report));
case ADMIN_USERS_FETCH_SUCCESS: case ADMIN_USERS_FETCH_SUCCESS:
return importUsers(state, action.users, action.params); return create(state, (draft) => importUsers(draft, action.users, action.params));
case ADMIN_USER_DELETE_SUCCESS: case ADMIN_USER_DELETE_SUCCESS:
return deleteUser(state, action.accountId); return create(state, (draft) => deleteUser(draft, action.accountId));
case ADMIN_USER_APPROVE_REQUEST: case ADMIN_USER_APPROVE_REQUEST:
return state.update('awaitingApproval', set => set.subtract(action.accountId)); return create(state, (draft) => {
draft.awaitingApproval = draft.awaitingApproval.filter(value => value !== action.accountId);
});
case ADMIN_USER_APPROVE_SUCCESS: case ADMIN_USER_APPROVE_SUCCESS:
return approveUser(state, action.user); return create(state, (draft) => approveUser(draft, action.user));
default: default:
return state; return state;
} }

View file

@ -1,8 +1,7 @@
import { import {
Map as ImmutableMap, Map as ImmutableMap,
Record as ImmutableRecord,
OrderedSet as ImmutableOrderedSet,
} from 'immutable'; } from 'immutable';
import { create } from 'mutative';
import { STATUS_IMPORT, STATUSES_IMPORT, type ImporterAction } from 'pl-fe/actions/importer'; import { STATUS_IMPORT, STATUSES_IMPORT, type ImporterAction } from 'pl-fe/actions/importer';
@ -18,49 +17,47 @@ import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
import type { Status } from 'pl-api'; import type { Status } from 'pl-api';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({ interface State {
inReplyTos: ImmutableMap<string, string>(), inReplyTos: Record<string, string>;
replies: ImmutableMap<string, ImmutableOrderedSet<string>>(), replies: Record<string, Array<string>>;
}); }
type State = ReturnType<typeof ReducerRecord>; const initialState: State = {
inReplyTos: {},
replies: {},
};
/** 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: Pick<Status, 'id' | 'in_reply_to_id'>, idempotencyKey?: string): State => { const importStatus = (state: State, status: Pick<Status, 'id' | 'in_reply_to_id'>, idempotencyKey?: string) => {
const { id, in_reply_to_id: inReplyToId } = status; const { id, in_reply_to_id: inReplyToId } = status;
if (!inReplyToId) return state; if (!inReplyToId) return;
return state.withMutations(state => { const replies = state.replies[inReplyToId] || [];
const replies = state.replies.get(inReplyToId) || ImmutableOrderedSet(); const newReplies = [...replies, id].toSorted();
const newReplies = replies.add(id).sort();
state.setIn(['replies', inReplyToId], newReplies); state.replies[inReplyToId] = newReplies;
state.setIn(['inReplyTos', id], inReplyToId); state.inReplyTos[id] = inReplyToId;
if (idempotencyKey) { if (idempotencyKey) {
deletePendingStatus(state, status, idempotencyKey); deletePendingStatus(state, status, idempotencyKey);
} }
});
}; };
/** Import multiple statuses into the state. */ /** Import multiple statuses into the state. */
const importStatuses = (state: State, statuses: Array<Pick<Status, 'id' | 'in_reply_to_id'>>): State => const importStatuses = (state: State, statuses: Array<Pick<Status, 'id' | 'in_reply_to_id'>>) =>
state.withMutations(state => { statuses.forEach(status => importStatus(state, status));
statuses.forEach(status => importStatus(state, status));
});
/** Insert a fake status ID connecting descendant to ancestor. */ /** Insert a fake status ID connecting descendant to ancestor. */
const insertTombstone = (state: State, ancestorId: string, descendantId: string): State => { const insertTombstone = (state: State, ancestorId: string, descendantId: string) => {
const tombstoneId = `${descendantId}-tombstone`; const tombstoneId = `${descendantId}-tombstone`;
return state.withMutations(state => {
importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId }); importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId });
importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId }); importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId });
});
}; };
/** Find the highest level status from this statusId. */ /** Find the highest level status from this statusId. */
const getRootNode = (state: State, statusId: string, initialId = statusId): string => { const getRootNode = (state: State, statusId: string, initialId = statusId): string => {
const parent = state.inReplyTos.get(statusId); const parent = state.inReplyTos[statusId];
if (!parent) { if (!parent) {
return statusId; return statusId;
@ -73,7 +70,7 @@ const getRootNode = (state: State, statusId: string, initialId = statusId): stri
}; };
/** Route fromId to toId by inserting tombstones. */ /** Route fromId to toId by inserting tombstones. */
const connectNodes = (state: State, fromId: string, toId: string): State => { const connectNodes = (state: State, fromId: string, toId: string) => {
const fromRoot = getRootNode(state, fromId); const fromRoot = getRootNode(state, fromId);
const toRoot = getRootNode(state, toId); const toRoot = getRootNode(state, toId);
@ -85,25 +82,23 @@ const connectNodes = (state: State, fromId: string, toId: string): State => {
}; };
/** Import a branch of ancestors or descendants, in relation to statusId. */ /** Import a branch of ancestors or descendants, in relation to statusId. */
const importBranch = (state: State, statuses: Array<Pick<Status, 'id' | 'in_reply_to_id'>>, statusId?: string): State => const importBranch = (state: State, statuses: Array<Pick<Status, 'id' | 'in_reply_to_id'>>, statusId?: string) =>
state.withMutations(state => { statuses.forEach((status, i) => {
statuses.forEach((status, i) => { const prevId = statusId && i === 0 ? statusId : (statuses[i - 1] || {}).id;
const prevId = statusId && i === 0 ? statusId : (statuses[i - 1] || {}).id;
if (status.in_reply_to_id) { if (status.in_reply_to_id) {
importStatus(state, status); importStatus(state, status);
// On Mastodon, in_reply_to_id can refer to an unavailable status, // On Mastodon, in_reply_to_id can refer to an unavailable status,
// so traverse the tree up and insert a connecting tombstone if needed. // so traverse the tree up and insert a connecting tombstone if needed.
if (statusId) { if (statusId) {
connectNodes(state, status.id, statusId); connectNodes(state, status.id, statusId);
}
} else if (prevId) {
// On Pleroma, in_reply_to_id will be null if the parent is unavailable,
// so insert the tombstone now.
insertTombstone(state, prevId, status.id);
} }
}); } else if (prevId) {
// On Pleroma, in_reply_to_id will be null if the parent is unavailable,
// so insert the tombstone now.
insertTombstone(state, prevId, status.id);
}
}); });
/** Import a status's ancestors and descendants. */ /** Import a status's ancestors and descendants. */
@ -112,39 +107,36 @@ const normalizeContext = (
id: string, id: string,
ancestors: Array<Pick<Status, 'id' | 'in_reply_to_id'>>, ancestors: Array<Pick<Status, 'id' | 'in_reply_to_id'>>,
descendants: Array<Pick<Status, 'id' | 'in_reply_to_id'>>, descendants: Array<Pick<Status, 'id' | 'in_reply_to_id'>>,
) => state.withMutations(state => { ) => {
importBranch(state, ancestors); importBranch(state, ancestors);
importBranch(state, descendants, id); importBranch(state, descendants, id);
if (ancestors.length > 0 && !state.getIn(['inReplyTos', id])) { if (ancestors.length > 0 && !state.inReplyTos[id]) {
insertTombstone(state, ancestors[ancestors.length - 1].id, id); insertTombstone(state, ancestors[ancestors.length - 1].id, id);
} }
}); };
/** Remove a status from the reducer. */ /** Remove a status from the reducer. */
const deleteStatus = (state: State, statusId: string): State => const deleteStatus = (state: State, statusId: string) => {
state.withMutations(state => { // Delete from its parent's tree
// Delete from its parent's tree const parentId = state.inReplyTos[statusId];
const parentId = state.inReplyTos.get(statusId); if (parentId) {
if (parentId) { const parentReplies = state.replies[parentId] || [];
const parentReplies = state.replies.get(parentId) || ImmutableOrderedSet(); const newParentReplies = parentReplies.filter(id => id !== statusId);
const newParentReplies = parentReplies.delete(statusId); state.replies[parentId] = newParentReplies;
state.setIn(['replies', parentId], newParentReplies); }
}
// Dereference children // Dereference children
const replies = state.replies.get(statusId) || ImmutableOrderedSet(); const replies = state.replies[statusId] = [];
replies.forEach(reply => state.deleteIn(['inReplyTos', reply])); replies.forEach(reply => delete state.inReplyTos[reply]);
state.deleteIn(['inReplyTos', statusId]); delete state.inReplyTos[statusId];
state.deleteIn(['replies', statusId]); delete state.replies[statusId];
}); };
/** Delete multiple statuses from the reducer. */ /** Delete multiple statuses from the reducer. */
const deleteStatuses = (state: State, statusIds: string[]): State => const deleteStatuses = (state: State, statusIds: string[]) =>
state.withMutations(state => { statusIds.forEach(statusId => deleteStatus(state, statusId));
statusIds.forEach(statusId => deleteStatus(state, statusId));
});
/** Delete statuses upon blocking or muting a user. */ /** Delete statuses upon blocking or muting a user. */
const filterContexts = ( const filterContexts = (
@ -152,63 +144,60 @@ const filterContexts = (
relationship: { id: string }, relationship: { id: string },
/** The entire statuses map from the store. */ /** The entire statuses map from the store. */
statuses: ImmutableMap<string, Status>, statuses: ImmutableMap<string, Status>,
): State => { ) => {
const ownedStatusIds = statuses const ownedStatusIds = statuses
.filter(status => status.account.id === relationship.id) .filter(status => status.account.id === relationship.id)
.map(status => status.id) .map(status => status.id)
.toList() .toList()
.toArray(); .toArray();
return deleteStatuses(state, ownedStatusIds); deleteStatuses(state, ownedStatusIds);
}; };
/** Add a fake status ID for a pending status. */ /** Add a fake status ID for a pending status. */
const importPendingStatus = (state: State, params: Pick<Status, 'id' | 'in_reply_to_id'>, idempotencyKey: string): State => { const importPendingStatus = (state: State, params: Pick<Status, 'id' | 'in_reply_to_id'>, idempotencyKey: string) => {
const id = `末pending-${idempotencyKey}`; const id = `末pending-${idempotencyKey}`;
const { in_reply_to_id } = params; const { in_reply_to_id } = params;
return importStatus(state, { id, in_reply_to_id }); return importStatus(state, { id, in_reply_to_id });
}; };
/** Delete a pending status from the reducer. */ /** Delete a pending status from the reducer. */
const deletePendingStatus = (state: State, params: Pick<Status, 'id' | 'in_reply_to_id'>, idempotencyKey: string): State => { const deletePendingStatus = (state: State, params: Pick<Status, 'id' | 'in_reply_to_id'>, idempotencyKey: string) => {
const id = `末pending-${idempotencyKey}`; const id = `末pending-${idempotencyKey}`;
const { in_reply_to_id: inReplyToId } = params; const { in_reply_to_id: inReplyToId } = params;
return state.withMutations(state => { delete state.inReplyTos[id];
state.deleteIn(['inReplyTos', id]);
if (inReplyToId) { if (inReplyToId) {
const replies = state.replies.get(inReplyToId) || ImmutableOrderedSet(); const replies = state.replies[inReplyToId] || [];
const newReplies = replies.delete(id).sort(); const newReplies = replies.filter(replyId => replyId !== id).toSorted();
state.setIn(['replies', inReplyToId], newReplies); state.replies[inReplyToId] = newReplies;
} }
});
}; };
/** Contexts reducer. Used for building a nested tree structure for threads. */ /** Contexts reducer. Used for building a nested tree structure for threads. */
const replies = (state = ReducerRecord(), action: AnyAction | ImporterAction | StatusesAction | TimelineAction) => { const replies = (state = initialState, action: AnyAction | ImporterAction | StatusesAction | TimelineAction): State => {
switch (action.type) { switch (action.type) {
case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_BLOCK_SUCCESS:
case ACCOUNT_MUTE_SUCCESS: case ACCOUNT_MUTE_SUCCESS:
return filterContexts(state, action.relationship, action.statuses); return create(state, (draft) => filterContexts(draft, action.relationship, action.statuses));
case CONTEXT_FETCH_SUCCESS: case CONTEXT_FETCH_SUCCESS:
return normalizeContext(state, action.statusId, action.ancestors, action.descendants); return create(state, (draft) => normalizeContext(draft, action.statusId, action.ancestors, action.descendants));
case TIMELINE_DELETE: case TIMELINE_DELETE:
return deleteStatuses(state, [action.statusId]); return create(state, (draft) => deleteStatuses(draft, [action.statusId]));
case STATUS_CREATE_REQUEST: case STATUS_CREATE_REQUEST:
return importPendingStatus(state, action.params, action.idempotencyKey); return create(state, (draft) => importPendingStatus(draft, action.params, action.idempotencyKey));
case STATUS_CREATE_SUCCESS: case STATUS_CREATE_SUCCESS:
return deletePendingStatus(state, action.status, action.idempotencyKey); return create(state, (draft) => deletePendingStatus(draft, action.status, action.idempotencyKey));
case STATUS_IMPORT: case STATUS_IMPORT:
return importStatus(state, action.status, action.idempotencyKey); return create(state, (draft) => importStatus(draft, action.status, action.idempotencyKey));
case STATUSES_IMPORT: case STATUSES_IMPORT:
return importStatuses(state, action.statuses); return create(state, (draft) => importStatuses(draft, action.statuses));
default: default:
return state; return state;
} }
}; };
export { export {
ReducerRecord,
replies as default, replies as default,
}; };

View file

@ -1,6 +1,6 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { create } from 'mutative'; import { create } from 'mutative';
import { type Instance, instanceSchema } from 'pl-api'; import { type Instance, instanceSchema, PleromaConfig } from 'pl-api';
import * as v from 'valibot'; import * as v from 'valibot';
import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'pl-fe/actions/admin'; import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'pl-fe/actions/admin';
@ -27,7 +27,7 @@ const getConfigValue = (instanceConfig: ImmutableMap<string, any>, key: string)
return v ? v.getIn(['tuple', 1]) : undefined; return v ? v.getIn(['tuple', 1]) : undefined;
}; };
const importConfigs = (state: State, configs: ImmutableList<any>) => { const importConfigs = (state: State, configs: PleromaConfig['configs']) => {
// FIXME: This is pretty hacked together. Need to make a cleaner map. // FIXME: This is pretty hacked together. Need to make a cleaner map.
const config = ConfigDB.find(configs, ':pleroma', ':instance'); const config = ConfigDB.find(configs, ':pleroma', ':instance');
const simplePolicy = ConfigDB.toSimplePolicy(configs); const simplePolicy = ConfigDB.toSimplePolicy(configs);
@ -35,7 +35,7 @@ const importConfigs = (state: State, configs: ImmutableList<any>) => {
if (!config && !simplePolicy) return state; if (!config && !simplePolicy) return state;
if (config) { if (config) {
const value = config.get('value', ImmutableList()); const value = config.value || [];
const registrationsOpen = getConfigValue(value, ':registrations_open') as boolean | undefined; const registrationsOpen = getConfigValue(value, ':registrations_open') as boolean | undefined;
const approvalRequired = getConfigValue(value, ':account_approval_required') as boolean | undefined; const approvalRequired = getConfigValue(value, ':account_approval_required') as boolean | undefined;
@ -97,7 +97,7 @@ const instance = (state = initialState, action: AnyAction | InstanceAction | Pre
return handleInstanceFetchFail(state, action.error); return handleInstanceFetchFail(state, action.error);
case ADMIN_CONFIG_UPDATE_REQUEST: case ADMIN_CONFIG_UPDATE_REQUEST:
case ADMIN_CONFIG_UPDATE_SUCCESS: case ADMIN_CONFIG_UPDATE_SUCCESS:
return create(state, (draft) => importConfigs(draft, ImmutableList(fromJS(action.configs)))); return create(state, (draft) => importConfigs(draft, action.configs));
default: default:
return state; return state;
} }

View file

@ -1,5 +1,3 @@
import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
import { PLEROMA_PRELOAD_IMPORT } from 'pl-fe/actions/preload'; import { PLEROMA_PRELOAD_IMPORT } from 'pl-fe/actions/preload';
import KVStore from 'pl-fe/storage/kv-store'; import KVStore from 'pl-fe/storage/kv-store';
import ConfigDB from 'pl-fe/utils/config-db'; import ConfigDB from 'pl-fe/utils/config-db';
@ -11,58 +9,60 @@ import {
PLFE_CONFIG_REQUEST_FAIL, PLFE_CONFIG_REQUEST_FAIL,
} from '../actions/pl-fe'; } from '../actions/pl-fe';
const initialState = ImmutableMap<string, any>(); import type { PleromaConfig } from 'pl-api';
const fallbackState = ImmutableMap<string, any>({ const initialState: Record<string, any> = {};
const fallbackState = {
brandColor: '#d80482', brandColor: '#d80482',
}); };
const updateFromAdmin = (state: ImmutableMap<string, any>, configs: ImmutableList<ImmutableMap<string, any>>) => { const updateFromAdmin = (state: Record<string, any>, configs: PleromaConfig['configs']) => {
try { try {
return ConfigDB.find(configs, ':pleroma', ':frontend_configurations')! return ConfigDB.find(configs, ':pleroma', ':frontend_configurations')!
.get('value') .value
.find((value: ImmutableMap<string, any>) => value.getIn(['tuple', 0]) === ':pl_fe') .find((value: Record<string, any>) => value.tuple?.[0] === ':pl_fe')
.getIn(['tuple', 1]); .tuple?.[1];
} catch { } catch {
return state; return state;
} }
}; };
const preloadImport = (state: ImmutableMap<string, any>, action: Record<string, any>) => { const preloadImport = (state: Record<string, any>, action: Record<string, any>) => {
const path = '/api/pleroma/frontend_configurations'; const path = '/api/pleroma/frontend_configurations';
const feData = action.data[path]; const feData = action.data[path];
if (feData) { if (feData) {
const plfe = feData.pl_fe; const plfe = feData.pl_fe;
return plfe ? fallbackState.mergeDeep(fromJS(plfe)) : fallbackState; return plfe ? { ...fallbackState, ...plfe } : fallbackState;
} else { } else {
return state; return state;
} }
}; };
const persistPlFeConfig = (plFeConfig: ImmutableMap<string, any>, host: string) => { const persistPlFeConfig = (plFeConfig: Record<string, any>, host: string) => {
if (host) { if (host) {
KVStore.setItem(`plfe_config:${host}`, plFeConfig.toJS()).catch(console.error); KVStore.setItem(`plfe_config:${host}`, plFeConfig).catch(console.error);
} }
}; };
const importPlFeConfig = (plFeConfig: ImmutableMap<string, any>, host: string) => { const importPlFeConfig = (plFeConfig: Record<string, any>, host: string) => {
persistPlFeConfig(plFeConfig, host); persistPlFeConfig(plFeConfig, host);
return plFeConfig; return plFeConfig;
}; };
const plfe = (state = initialState, action: Record<string, any>) => { const plfe = (state = initialState, action: Record<string, any>): Record<string, any> => {
switch (action.type) { switch (action.type) {
case PLEROMA_PRELOAD_IMPORT: case PLEROMA_PRELOAD_IMPORT:
return preloadImport(state, action); return preloadImport(state, action);
case PLFE_CONFIG_REMEMBER_SUCCESS: case PLFE_CONFIG_REMEMBER_SUCCESS:
return fromJS(action.plFeConfig); return action.plFeConfig;
case PLFE_CONFIG_REQUEST_SUCCESS: case PLFE_CONFIG_REQUEST_SUCCESS:
return importPlFeConfig(fromJS(action.plFeConfig) as ImmutableMap<string, any>, action.host); return importPlFeConfig(action.plFeConfig || {}, action.host);
case PLFE_CONFIG_REQUEST_FAIL: case PLFE_CONFIG_REQUEST_FAIL:
return fallbackState.mergeDeep(state); return { ...fallbackState, ...state };
case ADMIN_CONFIG_UPDATE_SUCCESS: case ADMIN_CONFIG_UPDATE_SUCCESS:
return updateFromAdmin(state, fromJS(action.configs) as ImmutableList<ImmutableMap<string, any>>); return updateFromAdmin(state, action.configs || []);
default: default:
return state; return state;
} }

View file

@ -1,5 +1,5 @@
import { Map as ImmutableMap } from 'immutable';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { create } from 'mutative';
import { normalizeStatus, Status as StatusRecord } from 'pl-fe/normalizers/status'; import { normalizeStatus, Status as StatusRecord } from 'pl-fe/normalizers/status';
import { simulateEmojiReact, simulateUnEmojiReact } from 'pl-fe/utils/emoji-reacts'; import { simulateEmojiReact, simulateUnEmojiReact } from 'pl-fe/utils/emoji-reacts';
@ -55,7 +55,7 @@ import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
import type { Status as BaseStatus, Translation } from 'pl-api'; import type { Status as BaseStatus, Translation } from 'pl-api';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
type State = ImmutableMap<string, MinifiedStatus>; type State = Record<string, MinifiedStatus>;
type MinifiedStatus = ReturnType<typeof minifyStatus>; type MinifiedStatus = ReturnType<typeof minifyStatus>;
@ -78,68 +78,58 @@ const fixQuote = (status: StatusRecord, oldStatus?: StatusRecord): StatusRecord
}; };
const fixStatus = (state: State, status: BaseStatus): MinifiedStatus => { const fixStatus = (state: State, status: BaseStatus): MinifiedStatus => {
const oldStatus = state.get(status.id); const oldStatus = state[status.id];
return minifyStatus(fixQuote(normalizeStatus(status, oldStatus))); return minifyStatus(fixQuote(normalizeStatus(status, oldStatus)));
}; };
const importStatus = (state: State, status: BaseStatus): State => const importStatus = (state: State, status: BaseStatus) =>{
state.set(status.id, fixStatus(state, status)); state[status.id] = fixStatus(state, status);
};
const importStatuses = (state: State, statuses: Array<BaseStatus>): State => const importStatuses = (state: State, statuses: Array<BaseStatus>) =>{
state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status))); statuses.forEach(status => importStatus(state, status));
};
const deleteStatus = (state: State, statusId: string, references: Array<string>) => { const deleteStatus = (state: State, statusId: string, references: Array<string>) => {
references.forEach(ref => { references.forEach(ref => {
state = deleteStatus(state, ref[0], []); deleteStatus(state, ref[0], []);
}); });
return state.delete(statusId); delete state[statusId];
}; };
const incrementReplyCount = (state: State, { in_reply_to_id, quote }: BaseStatus) => { const incrementReplyCount = (state: State, { in_reply_to_id, quote }: BaseStatus) => {
if (in_reply_to_id && state.has(in_reply_to_id)) { if (in_reply_to_id && state[in_reply_to_id]) {
const parent = state.get(in_reply_to_id)!; const parent = state[in_reply_to_id];
state = state.set(in_reply_to_id, { parent.replies_count = (typeof parent.replies_count === 'number' ? parent.replies_count : 0) + 1;
...parent,
replies_count: (typeof parent.replies_count === 'number' ? parent.replies_count : 0) + 1,
});
} }
if (quote?.id && state.has(quote.id)) { if (quote?.id && state[quote.id]) {
const parent = state.get(quote.id)!; const parent = state[quote.id];
state = state.set(quote.id, { parent.quotes_count = (typeof parent.quotes_count === 'number' ? parent.quotes_count : 0) + 1;
...parent,
quotes_count: (typeof parent.quotes_count === 'number' ? parent.quotes_count : 0) + 1,
});
} }
return state; return state;
}; };
const decrementReplyCount = (state: State, { in_reply_to_id, quote }: BaseStatus) => { const decrementReplyCount = (state: State, { in_reply_to_id, quote }: BaseStatus) => {
if (in_reply_to_id) { if (in_reply_to_id && state[in_reply_to_id]) {
state = state.updateIn([in_reply_to_id, 'replies_count'], 0, count => const parent = state[in_reply_to_id];
typeof count === 'number' ? Math.max(0, count - 1) : 0, parent.replies_count = Math.max(0, parent.replies_count - 1);
);
} }
if (quote?.id) { if (quote?.id) {
state = state.updateIn([quote.id, 'quotes_count'], 0, count => const parent = state[quote.id];
typeof count === 'number' ? Math.max(0, count - 1) : 0, parent.quotes_count = Math.max(0, parent.quotes_count - 1);
);
} }
return state; return state;
}; };
/** Simulate favourite/unfavourite of status for optimistic interactions */ /** Simulate favourite/unfavourite of status for optimistic interactions */
const simulateFavourite = ( const simulateFavourite = (state: State, statusId: string, favourited: boolean) => {
state: State, const status = state[statusId];
statusId: string,
favourited: boolean,
): State => {
const status = state.get(statusId);
if (!status) return state; if (!status) return state;
const delta = favourited ? +1 : -1; const delta = favourited ? +1 : -1;
@ -150,7 +140,7 @@ const simulateFavourite = (
favourites_count: Math.max(0, status.favourites_count + delta), favourites_count: Math.max(0, status.favourites_count + delta),
}; };
return state.set(statusId, updatedStatus); state[statusId] = updatedStatus;
}; };
/** Simulate dislike/undislike of status for optimistic interactions */ /** Simulate dislike/undislike of status for optimistic interactions */
@ -158,8 +148,8 @@ const simulateDislike = (
state: State, state: State,
statusId: string, statusId: string,
disliked: boolean, disliked: boolean,
): State => { ) => {
const status = state.get(statusId); const status = state[statusId];
if (!status) return state; if (!status) return state;
const delta = disliked ? +1 : -1; const delta = disliked ? +1 : -1;
@ -170,133 +160,212 @@ const simulateDislike = (
dislikes_count: Math.max(0, status.dislikes_count + delta), dislikes_count: Math.max(0, status.dislikes_count + delta),
}); });
return state.set(statusId, updatedStatus); state[statusId] = updatedStatus;
}; };
/** Import translation from translation service into the store. */ /** Import translation from translation service into the store. */
const importTranslation = (state: State, statusId: string, translation: Translation) => { const importTranslation = (state: State, statusId: string, translation: Translation) => {
return state.update(statusId, undefined as any, (status) => ({ if (!state[statusId]) return;
...status, state[statusId].translation = translation;
translation: translation, state[statusId].translating = false;
translating: false,
}));
}; };
/** Delete translation from the store. */ /** Delete translation from the store. */
const deleteTranslation = (state: State, statusId: string) => state.deleteIn([statusId, 'translation']); const deleteTranslation = (state: State, statusId: string) => {
state[statusId].translation = null;
};
const initialState: State = ImmutableMap(); const initialState: State = {};
const statuses = (state = initialState, action: AnyAction | EmojiReactsAction | EventsAction | ImporterAction | InteractionsAction | StatusesAction | TimelineAction): State => { const statuses = (state = initialState, action: AnyAction | EmojiReactsAction | EventsAction | ImporterAction | InteractionsAction | StatusesAction | TimelineAction): State => {
switch (action.type) { switch (action.type) {
case STATUS_IMPORT: case STATUS_IMPORT:
return importStatus(state, action.status); return create(state, (draft) => importStatus(draft, action.status));
case STATUSES_IMPORT: case STATUSES_IMPORT:
return importStatuses(state, action.statuses); return create(state, (draft) => importStatuses(draft, action.statuses));
case STATUS_CREATE_REQUEST: case STATUS_CREATE_REQUEST:
return action.editing ? state : incrementReplyCount(state, action.params); return action.editing ? state : create(state, (draft) => incrementReplyCount(draft, action.params));
case STATUS_CREATE_FAIL: case STATUS_CREATE_FAIL:
return action.editing ? state : decrementReplyCount(state, action.params); return action.editing ? state : create(state, (draft) => decrementReplyCount(draft, action.params));
case FAVOURITE_REQUEST: case FAVOURITE_REQUEST:
return simulateFavourite(state, action.statusId, true); return create(state, (draft) => simulateFavourite(draft, action.statusId, true));
case UNFAVOURITE_REQUEST: case UNFAVOURITE_REQUEST:
return simulateFavourite(state, action.statusId, false); return create(state, (draft) => simulateFavourite(draft, action.statusId, false));
case DISLIKE_REQUEST: case DISLIKE_REQUEST:
return simulateDislike(state, action.statusId, true); return create(state, (draft) => simulateDislike(draft, action.statusId, true));
case UNDISLIKE_REQUEST: case UNDISLIKE_REQUEST:
return simulateDislike(state, action.statusId, false); return create(state, (draft) => simulateDislike(draft, action.statusId, false));
case EMOJI_REACT_REQUEST: case EMOJI_REACT_REQUEST:
return state return create(state, (draft) => {
.updateIn( const status = draft[action.statusId];
[action.statusId, 'emoji_reactions'], if (status) {
emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji, action.custom), status.emoji_reactions = simulateEmojiReact(status.emoji_reactions, action.emoji, action.custom);
); }
});
case UNEMOJI_REACT_REQUEST: case UNEMOJI_REACT_REQUEST:
case EMOJI_REACT_FAIL: case EMOJI_REACT_FAIL:
return state return create(state, (draft) => {
.updateIn( const status = draft[action.statusId];
[action.statusId, 'emoji_reactions'], if (status) {
emojiReacts => simulateUnEmojiReact(emojiReacts as any, action.emoji), status.emoji_reactions = simulateUnEmojiReact(status.emoji_reactions, action.emoji);
); }
});
case FAVOURITE_FAIL: case FAVOURITE_FAIL:
return state.get(action.statusId) === undefined ? state : state.setIn([action.statusId, 'favourited'], false); return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.favourited = false;
}
});
case DISLIKE_FAIL: case DISLIKE_FAIL:
return state.get(action.statusId) === undefined ? state : state.setIn([action.statusId, 'disliked'], false); return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.disliked = false;
}
});
case REBLOG_REQUEST: case REBLOG_REQUEST:
return state return create(state, (draft) => {
.updateIn([action.statusId, 'reblogs_count'], 0, (count) => typeof count === 'number' ? count + 1 : 1) const status = draft[action.statusId];
.setIn([action.statusId, 'reblogged'], true); if (status) {
status.reblogs_count += 1;
status.reblogged = true;
}
});
case REBLOG_FAIL: case REBLOG_FAIL:
return state.get(action.statusId) === undefined ? state : state.setIn([action.statusId, 'reblogged'], false); return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.reblogged = false;
}
});
case UNREBLOG_REQUEST: case UNREBLOG_REQUEST:
return state return create(state, (draft) => {
.updateIn([action.statusId, 'reblogs_count'], 0, (count) => typeof count === 'number' ? Math.max(0, count - 1) : 0) const status = draft[action.statusId];
.setIn([action.statusId, 'reblogged'], false); if (status) {
status.reblogs_count = Math.max(0, status.reblogs_count - 1);
status.reblogged = false;
}
});
case UNREBLOG_FAIL: case UNREBLOG_FAIL:
return state.get(action.statusId) === undefined ? state : state.setIn([action.statusId, 'reblogged'], true); return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.reblogged = true;
}
});
case STATUS_MUTE_SUCCESS: case STATUS_MUTE_SUCCESS:
return state.setIn([action.statusId, 'muted'], true); return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.muted = true;
}
});
case STATUS_UNMUTE_SUCCESS: case STATUS_UNMUTE_SUCCESS:
return state.setIn([action.statusId, 'muted'], false); return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.muted = false;
}
});
case STATUS_REVEAL_MEDIA: case STATUS_REVEAL_MEDIA:
return state.withMutations(map => { return create(state, (draft) => {
action.statusIds.forEach((id: string) => { action.statusIds.forEach((id: string) => {
if (!(state.get(id) === undefined)) { const status = draft[id];
map.setIn([id, 'hidden'], false); if (status) {
status.hidden = false;
} }
}); });
}); });
case STATUS_HIDE_MEDIA: case STATUS_HIDE_MEDIA:
return state.withMutations(map => { return create(state, (draft) => {
action.statusIds.forEach((id: string) => { action.statusIds.forEach((id: string) => {
if (!(state.get(id) === undefined)) { const status = draft[id];
map.setIn([id, 'hidden'], true); if (status) {
status.hidden = true;
} }
}); });
}); });
case STATUS_EXPAND_SPOILER: case STATUS_EXPAND_SPOILER:
return state.withMutations(map => { return create(state, (draft) => {
action.statusIds.forEach((id: string) => { action.statusIds.forEach((id: string) => {
if (!(state.get(id) === undefined)) { const status = draft[id];
map.setIn([id, 'expanded'], true); if (status) {
status.expanded = true;
} }
}); });
}); });
case STATUS_COLLAPSE_SPOILER: case STATUS_COLLAPSE_SPOILER:
return state.withMutations(map => { return create(state, (draft) => {
action.statusIds.forEach((id: string) => { action.statusIds.forEach((id: string) => {
if (!(state.get(id) === undefined)) { const status = draft[id];
map.setIn([id, 'expanded'], false); if (status) {
status.expanded = false;
status.translation = false;
} }
}); });
}); });
case STATUS_DELETE_REQUEST: case STATUS_DELETE_REQUEST:
return decrementReplyCount(state, action.params); return create(state, (draft) => decrementReplyCount(draft, action.params));
case STATUS_DELETE_FAIL: case STATUS_DELETE_FAIL:
return incrementReplyCount(state, action.params); return create(state, (draft) => incrementReplyCount(draft, action.params));
case STATUS_TRANSLATE_REQUEST: case STATUS_TRANSLATE_REQUEST:
return state.setIn([action.statusId, 'translating'], true); return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.translating = true;
}
});
case STATUS_TRANSLATE_SUCCESS: case STATUS_TRANSLATE_SUCCESS:
return importTranslation(state, action.statusId, action.translation); return create(state, (draft) => importTranslation(draft, action.statusId, action.translation));
case STATUS_TRANSLATE_FAIL: case STATUS_TRANSLATE_FAIL:
return state return create(state, (draft) => {
.setIn([action.statusId, 'translating'], false) const status = draft[action.statusId];
.setIn([action.statusId, 'translation'], false); if (status) {
status.translating = false;
status.translation = false;
}
});
case STATUS_TRANSLATE_UNDO: case STATUS_TRANSLATE_UNDO:
return deleteTranslation(state, action.statusId); return create(state, (draft) => deleteTranslation(draft, action.statusId));
case STATUS_UNFILTER: case STATUS_UNFILTER:
return state.setIn([action.statusId, 'showFiltered'], false); return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.showFiltered = false;
}
});
case STATUS_LANGUAGE_CHANGE: case STATUS_LANGUAGE_CHANGE:
return state.setIn([action.statusId, 'currentLanguage'], action.language); return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.currentLanguage = action.language;
}
});
case TIMELINE_DELETE: case TIMELINE_DELETE:
return deleteStatus(state, action.statusId, action.references); return create(state, (draft) => deleteStatus(draft, action.statusId, action.references));
case EVENT_JOIN_REQUEST: case EVENT_JOIN_REQUEST:
return state.setIn([action.statusId, 'event', 'join_state'], 'pending'); return create(state, (draft) => {
const status = draft[action.statusId];
if (status?.event) {
status.event.join_state = 'pending';
}
});
case EVENT_JOIN_FAIL: case EVENT_JOIN_FAIL:
case EVENT_LEAVE_REQUEST: case EVENT_LEAVE_REQUEST:
return state.setIn([action.statusId, 'event', 'join_state'], null); return create(state, (draft) => {
const status = draft[action.statusId];
if (status?.event) {
status.event.join_state = null;
}
});
case EVENT_LEAVE_FAIL: case EVENT_LEAVE_FAIL:
return state.setIn([action.statusId, 'event', 'join_state'], action.previousState); return create(state, (draft) => {
const status = draft[action.statusId];
if (status?.event) {
status.event.join_state = action.previousState;
}
});
default: default:
return state; return state;
} }

View file

@ -1,11 +1,10 @@
import { import {
List as ImmutableList, List as ImmutableList,
OrderedSet as ImmutableOrderedSet, OrderedSet as ImmutableOrderedSet,
Record as ImmutableRecord,
} from 'immutable'; } from 'immutable';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { getLocale } from 'pl-fe/actions/settings'; // import { getLocale } from 'pl-fe/actions/settings';
import { Entities } from 'pl-fe/entity-store/entities'; import { Entities } from 'pl-fe/entity-store/entities';
import { useSettingsStore } from 'pl-fe/stores/settings'; import { useSettingsStore } from 'pl-fe/stores/settings';
import { getDomain } from 'pl-fe/utils/accounts'; import { getDomain } from 'pl-fe/utils/accounts';
@ -126,11 +125,11 @@ type APIStatus = { id: string; username?: string };
const makeGetStatus = () => createSelector( const makeGetStatus = () => createSelector(
[ [
(state: RootState, { id }: APIStatus) => state.statuses.get(id), (state: RootState, { id }: APIStatus) => state.statuses[id],
(state: RootState, { id }: APIStatus) => state.statuses.get(state.statuses.get(id)?.reblog_id || '', null), (state: RootState, { id }: APIStatus) => state.statuses[state.statuses[id]?.reblog_id || ''] || null,
(state: RootState, { id }: APIStatus) => state.statuses.get(state.statuses.get(id)?.quote_id || '', null), (state: RootState, { id }: APIStatus) => state.statuses[state.statuses[id]?.quote_id || ''] || null,
(state: RootState, { id }: APIStatus) => { (state: RootState, { id }: APIStatus) => {
const group = state.statuses.get(id)?.group_id; const group = state.statuses[id]?.group_id;
if (group) return state.entities[Entities.GROUPS]?.store[group] as Group; if (group) return state.entities[Entities.GROUPS]?.store[group] as Group;
return undefined; return undefined;
}, },
@ -139,10 +138,11 @@ const makeGetStatus = () => createSelector(
getFilters, getFilters,
(state: RootState) => state.me, (state: RootState) => state.me,
(state: RootState) => state.auth.client.features, (state: RootState) => state.auth.client.features,
(state: RootState) => getLocale('en'),
], ],
(statusBase, statusReblog, statusQuote, statusGroup, poll, username, filters, me, features, locale) => { (statusBase, statusReblog, statusQuote, statusGroup, poll, username, filters, me, features) => {
// const locale = getLocale('en');
if (!statusBase) return null; if (!statusBase) return null;
const { account } = statusBase; const { account } = statusBase;
const accountUsername = account.acct; const accountUsername = account.acct;
@ -181,7 +181,7 @@ const makeGetNotification = () => createSelector([
// @ts-ignore // @ts-ignore
(state: RootState, notification: NotificationGroup) => selectAccount(state, notification.target_id), (state: RootState, notification: NotificationGroup) => selectAccount(state, notification.target_id),
// @ts-ignore // @ts-ignore
(state: RootState, notification: NotificationGroup) => state.statuses.get(notification.status_id), (state: RootState, notification: NotificationGroup) => state.statuses[notification.status_id],
(state: RootState, notification: NotificationGroup) => selectAccounts(state, notification.sample_account_ids), (state: RootState, notification: NotificationGroup) => selectAccounts(state, notification.sample_account_ids),
], (notification, target, status, accounts): SelectedNotification => ({ ], (notification, target, status, accounts): SelectedNotification => ({
...notification, ...notification,
@ -213,28 +213,28 @@ const getAccountGallery = createSelector([
(state: RootState, id: string) => state.timelines.get(`account:${id}:with_replies:media`)?.items || ImmutableOrderedSet<string>(), (state: RootState, id: string) => state.timelines.get(`account:${id}:with_replies:media`)?.items || ImmutableOrderedSet<string>(),
(state: RootState) => state.statuses, (state: RootState) => state.statuses,
], (statusIds, statuses) => ], (statusIds, statuses) =>
statusIds.reduce((medias: ImmutableList<AccountGalleryAttachment>, statusId: string) => { statusIds.reduce((medias: Array<AccountGalleryAttachment>, statusId: string) => {
const status = statuses.get(statusId); const status = statuses[statusId];
if (!status) return medias; if (!status) return medias;
if (status.reblog_id) return medias; if (status.reblog_id) return medias;
return medias.concat( return medias.concat(
status.media_attachments.map(media => ({ ...media, status, account: status.account }))); status.media_attachments.map(media => ({ ...media, status, account: status.account })));
}, ImmutableList()), }, []),
); );
const getGroupGallery = createSelector([ const getGroupGallery = createSelector([
(state: RootState, id: string) => state.timelines.get(`group:${id}:media`)?.items || ImmutableOrderedSet<string>(), (state: RootState, id: string) => state.timelines.get(`group:${id}:media`)?.items || ImmutableOrderedSet<string>(),
(state: RootState) => state.statuses, (state: RootState) => state.statuses,
], (statusIds, statuses) => ], (statusIds, statuses) =>
statusIds.reduce((medias: ImmutableList<any>, statusId: string) => { statusIds.reduce((medias: Array<AccountGalleryAttachment>, statusId: string) => {
const status = statuses.get(statusId); const status = statuses[statusId];
if (!status) return medias; if (!status) return medias;
if (status.reblog_id) return medias; if (status.reblog_id) return medias;
return medias.concat( return medias.concat(
status.media_attachments.map(media => ({ ...media, status, account: status.account }))); status.media_attachments.map(media => ({ ...media, status, account: status.account })));
}, ImmutableList()), }, []),
); );
const makeGetReport = () => { const makeGetReport = () => {
@ -242,10 +242,10 @@ const makeGetReport = () => {
return createSelector( return createSelector(
[ [
(state: RootState, reportId: string) => state.admin.reports.get(reportId), (state: RootState, reportId: string) => state.admin.reports[reportId],
(state: RootState, reportId: string) => selectAccount(state, state.admin.reports.get(reportId)?.account_id || ''), (state: RootState, reportId: string) => selectAccount(state, state.admin.reports[reportId]?.account_id || ''),
(state: RootState, reportId: string) => selectAccount(state, state.admin.reports.get(reportId)?.target_account_id || ''), (state: RootState, reportId: string) => selectAccount(state, state.admin.reports[reportId]?.target_account_id || ''),
(state: RootState, reportId: string) => state.admin.reports.get(reportId)!.status_ids (state: RootState, reportId: string) => state.admin.reports[reportId]!.status_ids
.map((statusId) => getStatus(state, { id: statusId })) .map((statusId) => getStatus(state, { id: statusId }))
.filter((status): status is SelectedStatus => status !== null), .filter((status): status is SelectedStatus => status !== null),
], ],
@ -295,7 +295,7 @@ const getSimplePolicy = createSelector([
const getRemoteInstanceFavicon = (state: RootState, host: string) => { const getRemoteInstanceFavicon = (state: RootState, host: string) => {
const accounts = state.entities[Entities.ACCOUNTS]?.store as EntityStore<Account>; const accounts = state.entities[Entities.ACCOUNTS]?.store as EntityStore<Account>;
const account = Object.entries(accounts).find(([_, account]) => account && getDomain(account) === host)?.[1]; const account = Object.entries(accounts).find(([_, account]) => account && getDomain(account) === host)?.[1];
return account?.favicon; return account?.favicon || null;
}; };
type HostFederation = { type HostFederation = {
@ -319,25 +319,22 @@ const makeGetHosts = () =>
.sort(); .sort();
}); });
const RemoteInstanceRecord = ImmutableRecord({ interface RemoteInstance {
host: '', host: string;
favicon: null as string | null, favicon: string | null;
federation: null as unknown as HostFederation, federation: HostFederation;
}); }
type RemoteInstance = ReturnType<typeof RemoteInstanceRecord>;
const makeGetRemoteInstance = () => const makeGetRemoteInstance = () =>
createSelector([ createSelector([
(_state: RootState, host: string) => host, (_state: RootState, host: string) => host,
getRemoteInstanceFavicon, getRemoteInstanceFavicon,
getRemoteInstanceFederation, getRemoteInstanceFederation,
], (host, favicon, federation) => ], (host, favicon, federation): RemoteInstance => ({
RemoteInstanceRecord({ host,
host, favicon,
favicon, federation,
federation, }));
}));
type ColumnQuery = { type: string; prefix?: string }; type ColumnQuery = { type: string; prefix?: string };
@ -347,7 +344,7 @@ const makeGetStatusIds = () => createSelector([
(state: RootState) => state.statuses, (state: RootState) => state.statuses,
], (columnSettings: any, statusIds: ImmutableOrderedSet<string>, statuses) => ], (columnSettings: any, statusIds: ImmutableOrderedSet<string>, statuses) =>
statusIds.filter((id: string) => { statusIds.filter((id: string) => {
const status = statuses.get(id); const status = statuses[id];
if (!status) return true; if (!status) return true;
return !shouldFilter(status, columnSettings); return !shouldFilter(status, columnSettings);
}), }),

View file

@ -1,36 +1,33 @@
import {
Map as ImmutableMap,
List as ImmutableList,
Set as ImmutableSet,
} from 'immutable';
import trimStart from 'lodash/trimStart'; import trimStart from 'lodash/trimStart';
import * as v from 'valibot'; import * as v from 'valibot';
import { mrfSimpleSchema } from 'pl-fe/schemas/pleroma'; import { mrfSimpleSchema } from 'pl-fe/schemas/pleroma';
type Config = ImmutableMap<string, any>; import type { PleromaConfig } from 'pl-api';
type Policy = Record<string, any>; type Policy = Record<string, any>;
type Config = PleromaConfig['configs'][0];
const find = ( const find = (
configs: ImmutableList<Config>, configs: PleromaConfig['configs'],
group: string, group: string,
key: string, key: string,
): Config | undefined => configs.find(config => ): Config | undefined => configs.find(config =>
config.isSuperset(ImmutableMap({ group, key })), config.group === group && config.key === key,
); );
const toSimplePolicy = (configs: ImmutableList<Config>) => { const toSimplePolicy = (configs: PleromaConfig['configs']) => {
const config = find(configs, ':pleroma', ':mrf_simple'); const config = find(configs, ':pleroma', ':mrf_simple');
const reducer = (acc: ImmutableMap<string, any>, curr: ImmutableMap<string, any>) => { const reducer = (acc: Record<string, any>, curr: Record<string, any>) => {
const key = curr.getIn(['tuple', 0]) as string; const key = curr.tuple?.[0] as string;
const hosts = curr.getIn(['tuple', 1]) as ImmutableList<string>; const hosts = curr.tuple?.[1] as Array<string>;
return acc.set(trimStart(key, ':'), ImmutableSet(hosts)); return acc[trimStart(key, ':')] = hosts;
}; };
if (config?.get) { if (config) {
const value = config.get('value', ImmutableList()); const value = config.value || [];
const result = value.reduce(reducer, ImmutableMap()); const result = value.reduce(reducer, {});
return v.parse(mrfSimpleSchema, result.toJS()); return v.parse(mrfSimpleSchema, result.toJS());
} else { } else {
return v.parse(mrfSimpleSchema, {}); return v.parse(mrfSimpleSchema, {});
@ -38,7 +35,7 @@ const toSimplePolicy = (configs: ImmutableList<Config>) => {
}; };
const fromSimplePolicy = (simplePolicy: Policy) => { const fromSimplePolicy = (simplePolicy: Policy) => {
const mapper = ([key, hosts]: [key: string, hosts: ImmutableList<string>]) => ({ tuple: [`:${key}`, hosts] }); const mapper = ([key, hosts]: [key: string, hosts: Array<string>]) => ({ tuple: [`:${key}`, hosts] });
const value = Object.entries(simplePolicy).map(mapper); const value = Object.entries(simplePolicy).map(mapper);