pl-fe: Remove some immutable usage

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-05 23:32:43 +01:00
parent f560896311
commit 563e5288fb
41 changed files with 482 additions and 463 deletions

View file

@ -255,7 +255,7 @@ class PlApiClient {
unlisten: (listener: any) => void;
subscribe: (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, {

View file

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

View file

@ -162,7 +162,7 @@ const submitEventFail = (error: unknown) => ({
const joinEvent = (statusId: string, participationMessage?: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses.get(statusId);
const status = getState().statuses[statusId];
if (!status || !status.event || status.event.join_state) {
return dispatch(noOp);
@ -204,7 +204,7 @@ const joinEventFail = (error: unknown, statusId: string, previousState: string |
const leaveEvent = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses.get(statusId);
const status = getState().statuses[statusId];
if (!status || !status.event || !status.event.join_state) {
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 = () => {}) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const acct = state.statuses.get(statusId)!.account.acct;
const acct = state.statuses[statusId]!.account.acct;
useModalsStore.getState().openModal('CONFIRM', {
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 = () => {}) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const acct = state.statuses.get(statusId)!.account.acct;
const acct = state.statuses[statusId]!.account.acct;
useModalsStore.getState().openModal('CONFIRM', {
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>) =>
(dispatch: AppDispatch, getState: () => RootState) =>
dispatch(fetchConfig()).then(() => {
const configs = getState().admin.get('configs');
const configs = getState().admin.configs;
const simplePolicy = ConfigDB.toSimplePolicy(configs);
const merged = simplePolicyMerge(simplePolicy, host, restrictions);
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([
(state: RootState) => state.plfe,
(state: RootState) => state.auth.client.features,
], (plfe, features) => {
], (plfe) => {
// Do some additional normalization with the state
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 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;
dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
@ -133,7 +133,7 @@ const deleteStatus = (statusId: string, withRedraft = false) =>
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;
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 { useSettingsStore } from 'pl-fe/stores/settings';
import { shouldFilter } from 'pl-fe/utils/timelines';
@ -106,15 +104,17 @@ interface TimelineDeleteAction {
type: typeof TIMELINE_DELETE;
statusId: string;
accountId: string;
references: ImmutableMap<string, readonly [statusId: string, accountId: string]>;
references: Record<string, readonly [statusId: string, accountId: string]>;
reblogOf: string | null;
}
const deleteFromTimelines = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const accountId = getState().statuses.get(statusId)?.account?.id!;
const references = getState().statuses.filter(status => status.reblog_id === statusId).map(status => [status.id, status.account_id] as const);
const reblogOf = getState().statuses.get(statusId)?.reblog_id || null;
const accountId = getState().statuses[statusId]?.account?.id!;
const references = Object.fromEntries(Object.entries(getState().statuses)
.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>({
type: TIMELINE_DELETE,

View file

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

View file

@ -50,7 +50,7 @@ const SidebarNavigation = () => {
const notificationCount = useAppSelector((state) => state.notifications.unread);
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
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 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 status = useAppSelector(state => state.statuses.get(statusId!));
const status = useAppSelector(state => state.statuses[statusId!]);
useEffect(() => {
if (statusId && !status) {

View file

@ -1,4 +1,3 @@
import { List as ImmutableList } from 'immutable';
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router-dom';
@ -27,7 +26,7 @@ const AccountGallery = () => {
isUnavailable,
} = 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 hasMore = useAppSelector((state) => state.timelines.get(`account:${account?.id}:with_replies:media`)?.hasMore);
@ -81,7 +80,7 @@ const AccountGallery = () => {
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} />;
}
@ -103,11 +102,11 @@ const AccountGallery = () => {
key={`${attachment.status.id}+${attachment.id}`}
attachment={attachment}
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'>
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
</div>
@ -116,7 +115,7 @@ const AccountGallery = () => {
{loadOlder}
{isLoading && attachments.size === 0 && (
{isLoading && attachments.length === 0 && (
<div className='relative flex-auto px-8 py-4'>
<Spinner />
</div>

View file

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

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
@ -22,9 +21,9 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
const intl = useIntl();
const history = useHistory();
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(() => {
dispatch(fetchUsers({
@ -46,7 +45,7 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
onActionClick={handleAction}
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 />
))}
</Widget>

View file

@ -18,7 +18,7 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
const dispatch = useAppDispatch();
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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,8 @@
import { fromJS } from 'immutable';
import manifestMap from './manifest-map';
// All this does is converts the result from manifest_map.js into an ImmutableMap
const coinDB = fromJS(manifestMap);
import coinDB from './manifest-map';
/** Get title from CoinDB based on ticker symbol */
const getTitle = (ticker: string): string => {
const title = coinDB.getIn([ticker, 'name']);
const title = coinDB[ticker]?.name;
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 { FormattedMessage, useIntl } from 'react-intl';
@ -38,7 +37,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statu
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);
@ -61,12 +60,12 @@ const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statu
}, [isLoaded, me]);
const handleMoveUp = (id: string) => {
const index = ImmutableList(descendantsIds).indexOf(id);
const index = descendantsIds.indexOf(id);
_selectChild(index - 1);
};
const handleMoveDown = (id: string) => {
const index = ImmutableList(descendantsIds).indexOf(id);
const index = descendantsIds.indexOf(id);
_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')) {
return renderTombstone(id);
} 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) {
return (
@ -135,7 +134,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statu
const children: JSX.Element[] = [];
if (hasDescendants) {
children.push(...renderChildren(descendantsIds).toArray());
children.push(...renderChildren(descendantsIds));
}
return (

View file

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

View file

@ -52,7 +52,7 @@ interface IInteractionRequestStatus {
}
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;

View file

@ -128,7 +128,8 @@ const PlFeConfig: React.FC = () => {
};
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));
};
@ -257,7 +258,7 @@ const PlFeConfig: React.FC = () => {
<Input
type='text'
placeholder='/timeline/local'
value={String(data.get('redirectRootNoLogin', ''))}
value={String(data.redirectRootNoLogin || '')}
onChange={handleChange(['redirectRootNoLogin'], (e) => e.target.value)}
/>
</ListItem>
@ -269,7 +270,7 @@ const PlFeConfig: React.FC = () => {
<Input
type='text'
placeholder='https://01234abcdef@glitch.tip.tld/5678'
value={String(data.get('sentryDsn', ''))}
value={String(data.sentryDsn || '')}
onChange={handleChange(['sentryDsn'], (e) => e.target.value)}
/>
</ListItem>

View file

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

View file

@ -1,6 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import clsx from 'clsx';
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useCallback, useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet-async';
import { useIntl } from 'react-intl';
@ -35,36 +34,36 @@ const makeGetAncestorsIds = () => createSelector([
(_: RootState, statusId: string | undefined) => statusId,
(state: RootState) => state.contexts.inReplyTos,
], (statusId, inReplyTos) => {
let ancestorsIds = ImmutableOrderedSet<string>();
let ancestorsIds: Array<string> = [];
let id: string | undefined = statusId;
while (id && !ancestorsIds.includes(id)) {
ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds);
id = inReplyTos.get(id);
ancestorsIds = [id, ...ancestorsIds];
id = inReplyTos[id];
}
return ancestorsIds;
return [...new Set(ancestorsIds)];
});
const makeGetDescendantsIds = () => createSelector([
(_: RootState, statusId: string) => statusId,
(state: RootState) => state.contexts.replies,
], (statusId, contextReplies) => {
let descendantsIds = ImmutableOrderedSet<string>();
let descendantsIds: Array<string> = [];
const ids = [statusId];
while (ids.length > 0) {
const id = ids.shift();
if (!id) break;
const replies = contextReplies.get(id);
const replies = contextReplies[id];
if (descendantsIds.includes(id)) {
break;
}
if (statusId !== id) {
descendantsIds = descendantsIds.union([id]);
descendantsIds = [...descendantsIds, id];
}
if (replies) {
@ -74,7 +73,7 @@ const makeGetDescendantsIds = () => createSelector([
}
}
return descendantsIds;
return [...new Set(descendantsIds)];
});
const makeGetThread = () => {
@ -87,8 +86,8 @@ const makeGetThread = () => {
(_, statusId: string) => statusId,
],
(ancestorsIds, descendantsIds, statusId) => {
ancestorsIds = ancestorsIds.delete(statusId).subtract(descendantsIds);
descendantsIds = descendantsIds.delete(statusId).subtract(ancestorsIds);
ancestorsIds = ancestorsIds.filter(id => id !== statusId && !descendantsIds.includes(id));
descendantsIds = descendantsIds.filter(id => id !== statusId && !ancestorsIds.includes(id));
return {
ancestorsIds,
@ -121,8 +120,8 @@ const Thread: React.FC<IThread> = ({
const { ancestorsIds, descendantsIds } = useAppSelector((state) => getThread(state, status.id));
let initialIndex = ancestorsIds.size;
if (isModal && initialIndex !== 0) initialIndex = ancestorsIds.size + 1;
let initialIndex = ancestorsIds.length;
if (isModal && initialIndex !== 0) initialIndex = ancestorsIds.length + 1;
const node = useRef<HTMLDivElement>(null);
const statusRef = useRef<HTMLDivElement>(null);
@ -213,13 +212,13 @@ const Thread: React.FC<IThread> = ({
const handleMoveUp = (id: string) => {
if (id === status?.id) {
_selectChild(ancestorsIds.size - 1);
_selectChild(ancestorsIds.length - 1);
} else {
let index = ImmutableList(ancestorsIds).indexOf(id);
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = ImmutableList(descendantsIds).indexOf(id);
_selectChild(ancestorsIds.size + index);
index = descendantsIds.indexOf(id);
_selectChild(ancestorsIds.length + index);
} else {
_selectChild(index - 1);
}
@ -228,13 +227,13 @@ const Thread: React.FC<IThread> = ({
const handleMoveDown = (id: string) => {
if (id === status?.id) {
_selectChild(ancestorsIds.size + 1);
_selectChild(ancestorsIds.length + 1);
} else {
let index = ImmutableList(ancestorsIds).indexOf(id);
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = ImmutableList(descendantsIds).indexOf(id);
_selectChild(ancestorsIds.size + index + 2);
index = descendantsIds.indexOf(id);
_selectChild(ancestorsIds.length + index + 2);
} else {
_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')) {
return renderTombstone(id);
} else if (id.startsWith('末pending-')) {
@ -301,8 +300,8 @@ const Thread: React.FC<IThread> = ({
// Scroll focused status into view when thread updates.
useEffect(() => {
virtualizer.current?.scrollToIndex(ancestorsIds.size);
}, [status.id, ancestorsIds.size]);
virtualizer.current?.scrollToIndex(ancestorsIds.length);
}, [status.id, ancestorsIds.length]);
const handleOpenCompareHistoryModal = (status: Pick<Status, 'id'>) => {
openModal('COMPARE_HISTORY', {
@ -310,8 +309,8 @@ const Thread: React.FC<IThread> = ({
});
};
const hasAncestors = ancestorsIds.size > 0;
const hasDescendants = descendantsIds.size > 0;
const hasAncestors = ancestorsIds.length > 0;
const hasDescendants = descendantsIds.length > 0;
type HotkeyHandlers = { [key: string]: (keyEvent?: KeyboardEvent) => void };
@ -370,13 +369,13 @@ const Thread: React.FC<IThread> = ({
}
if (hasAncestors) {
children.push(...renderChildren(ancestorsIds).toArray());
children.push(...renderChildren(ancestorsIds));
}
children.push(focusedStatus);
if (hasDescendants) {
children.push(...renderChildren(descendantsIds).toArray());
children.push(...renderChildren(descendantsIds));
}
return (

View file

@ -25,7 +25,7 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
const loading = useAppSelector(state => state.history.getIn([statusId, 'loading']));
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 = () => {
onClose('COMPARE_HISTORY');

View file

@ -15,7 +15,7 @@ interface IStatusCheckBox {
}
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);

View file

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

View file

@ -16,7 +16,6 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { makeGetStatus } from 'pl-fe/selectors';
import type { BaseModalProps } from '../modal-root';
import type { Status as StatusEntity } from 'pl-fe/normalizers/status';
interface SelectBookmarkFolderModalProps {
statusId: string;
@ -24,7 +23,7 @@ interface SelectBookmarkFolderModalProps {
const SelectBookmarkFolderModal: React.FC<SelectBookmarkFolderModalProps & BaseModalProps> = ({ statusId, onClose }) => {
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 [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 { FormattedMessage } from 'react-intl';
@ -28,7 +27,7 @@ const GroupMediaPanel: React.FC<IGroupMediaPanel> = ({ group }) => {
const isMember = !!group?.relationship?.member;
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 => {
if (attachment.type === 'video') {
@ -55,7 +54,7 @@ const GroupMediaPanel: React.FC<IGroupMediaPanel> = ({ group }) => {
const renderAttachments = () => {
const nineAttachments = attachments.slice(0, 9);
if (!nineAttachments.isEmpty()) {
if (nineAttachments.length) {
return (
<div className='grid grid-cols-3 gap-0.5 overflow-hidden rounded-md'>
{nineAttachments.map((attachment, index) => (
@ -63,7 +62,7 @@ const GroupMediaPanel: React.FC<IGroupMediaPanel> = ({ group }) => {
key={`${attachment.status.id}+${attachment.id}`}
attachment={attachment}
onOpenMedia={handleOpenMedia}
isLast={index === nineAttachments.size - 1}
isLast={index === nineAttachments.length - 1}
/>
))}
</div>

View file

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

View file

@ -37,12 +37,12 @@ const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotency
account,
content: pendingStatus.status.replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
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,
media_attachments: (pendingStatus.media_ids || ImmutableList()).map((id: string) => ({ id })),
mentions: buildMentions(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,
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 { create } from 'mutative';
import {
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 { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
reports: ImmutableMap<string, MinifiedReport>(),
openReports: ImmutableOrderedSet<string>(),
users: ImmutableMap<string, MinifiedUser>(),
latestUsers: ImmutableOrderedSet<string>(),
awaitingApproval: ImmutableOrderedSet<string>(),
configs: ImmutableList<Config>(),
interface State {
reports: Record<string, MinifiedReport>;
openReports: Array<string>;
users: Record<string, MinifiedUser>;
latestUsers: Array<string>;
awaitingApproval: Array<string>;
configs: Array<Config>;
needsReboot: boolean;
}
const initialState: State = {
reports: {},
openReports: [],
users: {},
latestUsers: [],
awaitingApproval: [],
configs: [],
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 mergeSet = (state: State, key: SetKeys, users: Array<AdminAccount>): State => {
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 => {
const maybeImportUnapproved = (state: State, users: Array<AdminAccount>, params?: AdminGetAccountsParams) => {
if (params?.origin === 'local' && params.status === 'pending') {
return mergeSet(state, 'awaitingApproval', users);
const newIds = toIds(users);
state.awaitingApproval = [...new Set([...state.awaitingApproval, ...newIds])];
} else {
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') {
return replaceSet(state, 'latestUsers', users);
} else {
return state;
const newIds = toIds(users);
state.latestUsers = newIds;
}
};
@ -72,28 +59,26 @@ const minifyUser = (user: AdminAccount) => omit(user, ['account']);
type MinifiedUser = ReturnType<typeof minifyUser>;
const importUsers = (state: State, users: Array<AdminAccount>, params: AdminGetAccountsParams): State =>
state.withMutations(state => {
maybeImportUnapproved(state, users, params);
maybeImportLatest(state, users, params);
const importUsers = (state: State, users: Array<AdminAccount>, params: AdminGetAccountsParams) => {
maybeImportUnapproved(state, users, params);
maybeImportLatest(state, users, params);
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 => {
users.forEach(user => {
const normalizedUser = minifyUser(user);
state.update('awaitingApproval', orderedSet => orderedSet.delete(user.id));
state.setIn(['users', user.id], normalizedUser);
state.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(
report,
@ -102,53 +87,51 @@ const minifyReport = (report: AdminReport) => omit(
type MinifiedReport = ReturnType<typeof minifyReport>;
const importReports = (state: State, reports: Array<BaseAdminReport>): State =>
state.withMutations(state => {
reports.forEach(report => {
const minifiedReport = minifyReport(normalizeAdminReport(report));
if (!minifiedReport.action_taken) {
state.update('openReports', orderedSet => orderedSet.add(report.id));
}
state.setIn(['reports', report.id], minifiedReport);
});
const importReports = (state: State, reports: Array<BaseAdminReport>) => {
reports.forEach(report => {
const minifiedReport = minifyReport(normalizeAdminReport(report));
if (!minifiedReport.action_taken) {
state.openReports = [...new Set([...state.openReports, report.id])];
}
state.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
// hence the need for a new function.
state.withMutations(state => {
switch (report.action_taken) {
case false:
state.update('openReports', orderedSet => orderedSet.add(report.id));
break;
default:
state.update('openReports', orderedSet => orderedSet.delete(report.id));
}
});
switch (report.action_taken) {
case false:
state.openReports = [...new Set([...state.openReports, report.id])];
break;
default:
state.openReports = state.openReports.filter(id => id !== 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 importConfigs = (state: State, configs: any): State => state.set('configs', normalizeConfigs(configs));
const admin = (state: State = ReducerRecord(), action: AnyAction): State => {
const admin = (state = initialState, action: AnyAction): State => {
switch (action.type) {
case ADMIN_CONFIG_FETCH_SUCCESS:
case ADMIN_CONFIG_UPDATE_SUCCESS:
return importConfigs(state, action.configs);
return create(state, (draft) => importConfigs(draft, action.configs));
case ADMIN_REPORTS_FETCH_SUCCESS:
return importReports(state, action.reports);
return create(state, (draft) => importReports(draft, action.reports));
case ADMIN_REPORT_PATCH_SUCCESS:
return handleReportDiffs(state, action.report);
return create(state, (draft) => handleReportDiffs(draft, action.report));
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:
return deleteUser(state, action.accountId);
return create(state, (draft) => deleteUser(draft, action.accountId));
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:
return approveUser(state, action.user);
return create(state, (draft) => approveUser(draft, action.user));
default:
return state;
}

View file

@ -1,8 +1,7 @@
import {
Map as ImmutableMap,
Record as ImmutableRecord,
OrderedSet as ImmutableOrderedSet,
} from 'immutable';
import { create } from 'mutative';
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 { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
inReplyTos: ImmutableMap<string, string>(),
replies: ImmutableMap<string, ImmutableOrderedSet<string>>(),
});
interface State {
inReplyTos: Record<string, 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. */
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;
if (!inReplyToId) return state;
if (!inReplyToId) return;
return state.withMutations(state => {
const replies = state.replies.get(inReplyToId) || ImmutableOrderedSet();
const newReplies = replies.add(id).sort();
const replies = state.replies[inReplyToId] || [];
const newReplies = [...replies, id].toSorted();
state.setIn(['replies', inReplyToId], newReplies);
state.setIn(['inReplyTos', id], inReplyToId);
state.replies[inReplyToId] = newReplies;
state.inReplyTos[id] = inReplyToId;
if (idempotencyKey) {
deletePendingStatus(state, status, idempotencyKey);
}
});
if (idempotencyKey) {
deletePendingStatus(state, status, idempotencyKey);
}
};
/** Import multiple statuses into the state. */
const importStatuses = (state: State, statuses: Array<Pick<Status, 'id' | 'in_reply_to_id'>>): State =>
state.withMutations(state => {
statuses.forEach(status => importStatus(state, status));
});
const importStatuses = (state: State, statuses: Array<Pick<Status, 'id' | 'in_reply_to_id'>>) =>
statuses.forEach(status => importStatus(state, status));
/** 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`;
return state.withMutations(state => {
importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId });
importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId });
});
importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId });
importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId });
};
/** Find the highest level status from this statusId. */
const getRootNode = (state: State, statusId: string, initialId = statusId): string => {
const parent = state.inReplyTos.get(statusId);
const parent = state.inReplyTos[statusId];
if (!parent) {
return statusId;
@ -73,7 +70,7 @@ const getRootNode = (state: State, statusId: string, initialId = statusId): stri
};
/** 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 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. */
const importBranch = (state: State, statuses: Array<Pick<Status, 'id' | 'in_reply_to_id'>>, statusId?: string): State =>
state.withMutations(state => {
statuses.forEach((status, i) => {
const prevId = statusId && i === 0 ? statusId : (statuses[i - 1] || {}).id;
const importBranch = (state: State, statuses: Array<Pick<Status, 'id' | 'in_reply_to_id'>>, statusId?: string) =>
statuses.forEach((status, i) => {
const prevId = statusId && i === 0 ? statusId : (statuses[i - 1] || {}).id;
if (status.in_reply_to_id) {
importStatus(state, status);
if (status.in_reply_to_id) {
importStatus(state, 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.
if (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);
// On Mastodon, in_reply_to_id can refer to an unavailable status,
// so traverse the tree up and insert a connecting tombstone if needed.
if (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);
}
});
/** Import a status's ancestors and descendants. */
@ -112,39 +107,36 @@ const normalizeContext = (
id: string,
ancestors: 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, 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);
}
});
};
/** Remove a status from the reducer. */
const deleteStatus = (state: State, statusId: string): State =>
state.withMutations(state => {
// Delete from its parent's tree
const parentId = state.inReplyTos.get(statusId);
if (parentId) {
const parentReplies = state.replies.get(parentId) || ImmutableOrderedSet();
const newParentReplies = parentReplies.delete(statusId);
state.setIn(['replies', parentId], newParentReplies);
}
const deleteStatus = (state: State, statusId: string) => {
// Delete from its parent's tree
const parentId = state.inReplyTos[statusId];
if (parentId) {
const parentReplies = state.replies[parentId] || [];
const newParentReplies = parentReplies.filter(id => id !== statusId);
state.replies[parentId] = newParentReplies;
}
// Dereference children
const replies = state.replies.get(statusId) || ImmutableOrderedSet();
replies.forEach(reply => state.deleteIn(['inReplyTos', reply]));
// Dereference children
const replies = state.replies[statusId] = [];
replies.forEach(reply => delete state.inReplyTos[reply]);
state.deleteIn(['inReplyTos', statusId]);
state.deleteIn(['replies', statusId]);
});
delete state.inReplyTos[statusId];
delete state.replies[statusId];
};
/** Delete multiple statuses from the reducer. */
const deleteStatuses = (state: State, statusIds: string[]): State =>
state.withMutations(state => {
statusIds.forEach(statusId => deleteStatus(state, statusId));
});
const deleteStatuses = (state: State, statusIds: string[]) =>
statusIds.forEach(statusId => deleteStatus(state, statusId));
/** Delete statuses upon blocking or muting a user. */
const filterContexts = (
@ -152,63 +144,60 @@ const filterContexts = (
relationship: { id: string },
/** The entire statuses map from the store. */
statuses: ImmutableMap<string, Status>,
): State => {
) => {
const ownedStatusIds = statuses
.filter(status => status.account.id === relationship.id)
.map(status => status.id)
.toList()
.toArray();
return deleteStatuses(state, ownedStatusIds);
deleteStatuses(state, ownedStatusIds);
};
/** 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 { in_reply_to_id } = params;
return importStatus(state, { id, in_reply_to_id });
};
/** 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 { in_reply_to_id: inReplyToId } = params;
return state.withMutations(state => {
state.deleteIn(['inReplyTos', id]);
delete state.inReplyTos[id];
if (inReplyToId) {
const replies = state.replies.get(inReplyToId) || ImmutableOrderedSet();
const newReplies = replies.delete(id).sort();
state.setIn(['replies', inReplyToId], newReplies);
}
});
if (inReplyToId) {
const replies = state.replies[inReplyToId] || [];
const newReplies = replies.filter(replyId => replyId !== id).toSorted();
state.replies[inReplyToId] = newReplies;
}
};
/** 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) {
case ACCOUNT_BLOCK_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:
return normalizeContext(state, action.statusId, action.ancestors, action.descendants);
return create(state, (draft) => normalizeContext(draft, action.statusId, action.ancestors, action.descendants));
case TIMELINE_DELETE:
return deleteStatuses(state, [action.statusId]);
return create(state, (draft) => deleteStatuses(draft, [action.statusId]));
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:
return deletePendingStatus(state, action.status, action.idempotencyKey);
return create(state, (draft) => deletePendingStatus(draft, action.status, action.idempotencyKey));
case STATUS_IMPORT:
return importStatus(state, action.status, action.idempotencyKey);
return create(state, (draft) => importStatus(draft, action.status, action.idempotencyKey));
case STATUSES_IMPORT:
return importStatuses(state, action.statuses);
return create(state, (draft) => importStatuses(draft, action.statuses));
default:
return state;
}
};
export {
ReducerRecord,
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 { type Instance, instanceSchema } from 'pl-api';
import { type Instance, instanceSchema, PleromaConfig } from 'pl-api';
import * as v from 'valibot';
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;
};
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.
const config = ConfigDB.find(configs, ':pleroma', ':instance');
const simplePolicy = ConfigDB.toSimplePolicy(configs);
@ -35,7 +35,7 @@ const importConfigs = (state: State, configs: ImmutableList<any>) => {
if (!config && !simplePolicy) return state;
if (config) {
const value = config.get('value', ImmutableList());
const value = config.value || [];
const registrationsOpen = getConfigValue(value, ':registrations_open') 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);
case ADMIN_CONFIG_UPDATE_REQUEST:
case ADMIN_CONFIG_UPDATE_SUCCESS:
return create(state, (draft) => importConfigs(draft, ImmutableList(fromJS(action.configs))));
return create(state, (draft) => importConfigs(draft, action.configs));
default:
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 KVStore from 'pl-fe/storage/kv-store';
import ConfigDB from 'pl-fe/utils/config-db';
@ -11,58 +9,60 @@ import {
PLFE_CONFIG_REQUEST_FAIL,
} 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',
});
};
const updateFromAdmin = (state: ImmutableMap<string, any>, configs: ImmutableList<ImmutableMap<string, any>>) => {
const updateFromAdmin = (state: Record<string, any>, configs: PleromaConfig['configs']) => {
try {
return ConfigDB.find(configs, ':pleroma', ':frontend_configurations')!
.get('value')
.find((value: ImmutableMap<string, any>) => value.getIn(['tuple', 0]) === ':pl_fe')
.getIn(['tuple', 1]);
.value
.find((value: Record<string, any>) => value.tuple?.[0] === ':pl_fe')
.tuple?.[1];
} catch {
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 feData = action.data[path];
if (feData) {
const plfe = feData.pl_fe;
return plfe ? fallbackState.mergeDeep(fromJS(plfe)) : fallbackState;
return plfe ? { ...fallbackState, ...plfe } : fallbackState;
} else {
return state;
}
};
const persistPlFeConfig = (plFeConfig: ImmutableMap<string, any>, host: string) => {
const persistPlFeConfig = (plFeConfig: Record<string, any>, host: string) => {
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);
return plFeConfig;
};
const plfe = (state = initialState, action: Record<string, any>) => {
const plfe = (state = initialState, action: Record<string, any>): Record<string, any> => {
switch (action.type) {
case PLEROMA_PRELOAD_IMPORT:
return preloadImport(state, action);
case PLFE_CONFIG_REMEMBER_SUCCESS:
return fromJS(action.plFeConfig);
return action.plFeConfig;
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:
return fallbackState.mergeDeep(state);
return { ...fallbackState, ...state };
case ADMIN_CONFIG_UPDATE_SUCCESS:
return updateFromAdmin(state, fromJS(action.configs) as ImmutableList<ImmutableMap<string, any>>);
return updateFromAdmin(state, action.configs || []);
default:
return state;
}

View file

@ -1,5 +1,5 @@
import { Map as ImmutableMap } from 'immutable';
import omit from 'lodash/omit';
import { create } from 'mutative';
import { normalizeStatus, Status as StatusRecord } from 'pl-fe/normalizers/status';
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 { AnyAction } from 'redux';
type State = ImmutableMap<string, MinifiedStatus>;
type State = Record<string, MinifiedStatus>;
type MinifiedStatus = ReturnType<typeof minifyStatus>;
@ -78,68 +78,58 @@ const fixQuote = (status: StatusRecord, oldStatus?: StatusRecord): StatusRecord
};
const fixStatus = (state: State, status: BaseStatus): MinifiedStatus => {
const oldStatus = state.get(status.id);
const oldStatus = state[status.id];
return minifyStatus(fixQuote(normalizeStatus(status, oldStatus)));
};
const importStatus = (state: State, status: BaseStatus): State =>
state.set(status.id, fixStatus(state, status));
const importStatus = (state: State, status: BaseStatus) =>{
state[status.id] = fixStatus(state, status);
};
const importStatuses = (state: State, statuses: Array<BaseStatus>): State =>
state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status)));
const importStatuses = (state: State, statuses: Array<BaseStatus>) =>{
statuses.forEach(status => importStatus(state, status));
};
const deleteStatus = (state: State, statusId: string, references: Array<string>) => {
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) => {
if (in_reply_to_id && state.has(in_reply_to_id)) {
const parent = state.get(in_reply_to_id)!;
state = state.set(in_reply_to_id, {
...parent,
replies_count: (typeof parent.replies_count === 'number' ? parent.replies_count : 0) + 1,
});
if (in_reply_to_id && state[in_reply_to_id]) {
const parent = state[in_reply_to_id];
parent.replies_count = (typeof parent.replies_count === 'number' ? parent.replies_count : 0) + 1;
}
if (quote?.id && state.has(quote.id)) {
const parent = state.get(quote.id)!;
state = state.set(quote.id, {
...parent,
quotes_count: (typeof parent.quotes_count === 'number' ? parent.quotes_count : 0) + 1,
});
if (quote?.id && state[quote.id]) {
const parent = state[quote.id];
parent.quotes_count = (typeof parent.quotes_count === 'number' ? parent.quotes_count : 0) + 1;
}
return state;
};
const decrementReplyCount = (state: State, { in_reply_to_id, quote }: BaseStatus) => {
if (in_reply_to_id) {
state = state.updateIn([in_reply_to_id, 'replies_count'], 0, count =>
typeof count === 'number' ? Math.max(0, count - 1) : 0,
);
if (in_reply_to_id && state[in_reply_to_id]) {
const parent = state[in_reply_to_id];
parent.replies_count = Math.max(0, parent.replies_count - 1);
}
if (quote?.id) {
state = state.updateIn([quote.id, 'quotes_count'], 0, count =>
typeof count === 'number' ? Math.max(0, count - 1) : 0,
);
const parent = state[quote.id];
parent.quotes_count = Math.max(0, parent.quotes_count - 1);
}
return state;
};
/** Simulate favourite/unfavourite of status for optimistic interactions */
const simulateFavourite = (
state: State,
statusId: string,
favourited: boolean,
): State => {
const status = state.get(statusId);
const simulateFavourite = (state: State, statusId: string, favourited: boolean) => {
const status = state[statusId];
if (!status) return state;
const delta = favourited ? +1 : -1;
@ -150,7 +140,7 @@ const simulateFavourite = (
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 */
@ -158,8 +148,8 @@ const simulateDislike = (
state: State,
statusId: string,
disliked: boolean,
): State => {
const status = state.get(statusId);
) => {
const status = state[statusId];
if (!status) return state;
const delta = disliked ? +1 : -1;
@ -170,133 +160,212 @@ const simulateDislike = (
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. */
const importTranslation = (state: State, statusId: string, translation: Translation) => {
return state.update(statusId, undefined as any, (status) => ({
...status,
translation: translation,
translating: false,
}));
if (!state[statusId]) return;
state[statusId].translation = translation;
state[statusId].translating = false;
};
/** 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 => {
switch (action.type) {
case STATUS_IMPORT:
return importStatus(state, action.status);
return create(state, (draft) => importStatus(draft, action.status));
case STATUSES_IMPORT:
return importStatuses(state, action.statuses);
return create(state, (draft) => importStatuses(draft, action.statuses));
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:
return action.editing ? state : decrementReplyCount(state, action.params);
return action.editing ? state : create(state, (draft) => decrementReplyCount(draft, action.params));
case FAVOURITE_REQUEST:
return simulateFavourite(state, action.statusId, true);
return create(state, (draft) => simulateFavourite(draft, action.statusId, true));
case UNFAVOURITE_REQUEST:
return simulateFavourite(state, action.statusId, false);
return create(state, (draft) => simulateFavourite(draft, action.statusId, false));
case DISLIKE_REQUEST:
return simulateDislike(state, action.statusId, true);
return create(state, (draft) => simulateDislike(draft, action.statusId, true));
case UNDISLIKE_REQUEST:
return simulateDislike(state, action.statusId, false);
return create(state, (draft) => simulateDislike(draft, action.statusId, false));
case EMOJI_REACT_REQUEST:
return state
.updateIn(
[action.statusId, 'emoji_reactions'],
emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji, action.custom),
);
return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.emoji_reactions = simulateEmojiReact(status.emoji_reactions, action.emoji, action.custom);
}
});
case UNEMOJI_REACT_REQUEST:
case EMOJI_REACT_FAIL:
return state
.updateIn(
[action.statusId, 'emoji_reactions'],
emojiReacts => simulateUnEmojiReact(emojiReacts as any, action.emoji),
);
return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.emoji_reactions = simulateUnEmojiReact(status.emoji_reactions, action.emoji);
}
});
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:
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:
return state
.updateIn([action.statusId, 'reblogs_count'], 0, (count) => typeof count === 'number' ? count + 1 : 1)
.setIn([action.statusId, 'reblogged'], true);
return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.reblogs_count += 1;
status.reblogged = true;
}
});
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:
return state
.updateIn([action.statusId, 'reblogs_count'], 0, (count) => typeof count === 'number' ? Math.max(0, count - 1) : 0)
.setIn([action.statusId, 'reblogged'], false);
return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.reblogs_count = Math.max(0, status.reblogs_count - 1);
status.reblogged = false;
}
});
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:
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:
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:
return state.withMutations(map => {
return create(state, (draft) => {
action.statusIds.forEach((id: string) => {
if (!(state.get(id) === undefined)) {
map.setIn([id, 'hidden'], false);
const status = draft[id];
if (status) {
status.hidden = false;
}
});
});
case STATUS_HIDE_MEDIA:
return state.withMutations(map => {
return create(state, (draft) => {
action.statusIds.forEach((id: string) => {
if (!(state.get(id) === undefined)) {
map.setIn([id, 'hidden'], true);
const status = draft[id];
if (status) {
status.hidden = true;
}
});
});
case STATUS_EXPAND_SPOILER:
return state.withMutations(map => {
return create(state, (draft) => {
action.statusIds.forEach((id: string) => {
if (!(state.get(id) === undefined)) {
map.setIn([id, 'expanded'], true);
const status = draft[id];
if (status) {
status.expanded = true;
}
});
});
case STATUS_COLLAPSE_SPOILER:
return state.withMutations(map => {
return create(state, (draft) => {
action.statusIds.forEach((id: string) => {
if (!(state.get(id) === undefined)) {
map.setIn([id, 'expanded'], false);
const status = draft[id];
if (status) {
status.expanded = false;
status.translation = false;
}
});
});
case STATUS_DELETE_REQUEST:
return decrementReplyCount(state, action.params);
return create(state, (draft) => decrementReplyCount(draft, action.params));
case STATUS_DELETE_FAIL:
return incrementReplyCount(state, action.params);
return create(state, (draft) => incrementReplyCount(draft, action.params));
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:
return importTranslation(state, action.statusId, action.translation);
return create(state, (draft) => importTranslation(draft, action.statusId, action.translation));
case STATUS_TRANSLATE_FAIL:
return state
.setIn([action.statusId, 'translating'], false)
.setIn([action.statusId, 'translation'], false);
return create(state, (draft) => {
const status = draft[action.statusId];
if (status) {
status.translating = false;
status.translation = false;
}
});
case STATUS_TRANSLATE_UNDO:
return deleteTranslation(state, action.statusId);
return create(state, (draft) => deleteTranslation(draft, action.statusId));
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:
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:
return deleteStatus(state, action.statusId, action.references);
return create(state, (draft) => deleteStatus(draft, action.statusId, action.references));
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_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:
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:
return state;
}

View file

@ -1,11 +1,10 @@
import {
List as ImmutableList,
OrderedSet as ImmutableOrderedSet,
Record as ImmutableRecord,
} from 'immutable';
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 { useSettingsStore } from 'pl-fe/stores/settings';
import { getDomain } from 'pl-fe/utils/accounts';
@ -126,11 +125,11 @@ type APIStatus = { id: string; username?: string };
const makeGetStatus = () => createSelector(
[
(state: RootState, { id }: APIStatus) => state.statuses.get(id),
(state: RootState, { id }: APIStatus) => state.statuses.get(state.statuses.get(id)?.reblog_id || '', null),
(state: RootState, { id }: APIStatus) => state.statuses.get(state.statuses.get(id)?.quote_id || '', null),
(state: RootState, { id }: APIStatus) => state.statuses[id],
(state: RootState, { id }: APIStatus) => state.statuses[state.statuses[id]?.reblog_id || ''] || null,
(state: RootState, { id }: APIStatus) => state.statuses[state.statuses[id]?.quote_id || ''] || null,
(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;
return undefined;
},
@ -139,10 +138,11 @@ const makeGetStatus = () => createSelector(
getFilters,
(state: RootState) => state.me,
(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;
const { account } = statusBase;
const accountUsername = account.acct;
@ -181,7 +181,7 @@ const makeGetNotification = () => createSelector([
// @ts-ignore
(state: RootState, notification: NotificationGroup) => selectAccount(state, notification.target_id),
// @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),
], (notification, target, status, accounts): SelectedNotification => ({
...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) => state.statuses,
], (statusIds, statuses) =>
statusIds.reduce((medias: ImmutableList<AccountGalleryAttachment>, statusId: string) => {
const status = statuses.get(statusId);
statusIds.reduce((medias: Array<AccountGalleryAttachment>, statusId: string) => {
const status = statuses[statusId];
if (!status) return medias;
if (status.reblog_id) return medias;
return medias.concat(
status.media_attachments.map(media => ({ ...media, status, account: status.account })));
}, ImmutableList()),
}, []),
);
const getGroupGallery = createSelector([
(state: RootState, id: string) => state.timelines.get(`group:${id}:media`)?.items || ImmutableOrderedSet<string>(),
(state: RootState) => state.statuses,
], (statusIds, statuses) =>
statusIds.reduce((medias: ImmutableList<any>, statusId: string) => {
const status = statuses.get(statusId);
statusIds.reduce((medias: Array<AccountGalleryAttachment>, statusId: string) => {
const status = statuses[statusId];
if (!status) return medias;
if (status.reblog_id) return medias;
return medias.concat(
status.media_attachments.map(media => ({ ...media, status, account: status.account })));
}, ImmutableList()),
}, []),
);
const makeGetReport = () => {
@ -242,10 +242,10 @@ const makeGetReport = () => {
return createSelector(
[
(state: RootState, reportId: string) => state.admin.reports.get(reportId),
(state: RootState, reportId: string) => selectAccount(state, state.admin.reports.get(reportId)?.account_id || ''),
(state: RootState, reportId: string) => selectAccount(state, state.admin.reports.get(reportId)?.target_account_id || ''),
(state: RootState, reportId: string) => state.admin.reports.get(reportId)!.status_ids
(state: RootState, reportId: string) => state.admin.reports[reportId],
(state: RootState, reportId: string) => selectAccount(state, state.admin.reports[reportId]?.account_id || ''),
(state: RootState, reportId: string) => selectAccount(state, state.admin.reports[reportId]?.target_account_id || ''),
(state: RootState, reportId: string) => state.admin.reports[reportId]!.status_ids
.map((statusId) => getStatus(state, { id: statusId }))
.filter((status): status is SelectedStatus => status !== null),
],
@ -295,7 +295,7 @@ const getSimplePolicy = createSelector([
const getRemoteInstanceFavicon = (state: RootState, host: string) => {
const accounts = state.entities[Entities.ACCOUNTS]?.store as EntityStore<Account>;
const account = Object.entries(accounts).find(([_, account]) => account && getDomain(account) === host)?.[1];
return account?.favicon;
return account?.favicon || null;
};
type HostFederation = {
@ -319,25 +319,22 @@ const makeGetHosts = () =>
.sort();
});
const RemoteInstanceRecord = ImmutableRecord({
host: '',
favicon: null as string | null,
federation: null as unknown as HostFederation,
});
type RemoteInstance = ReturnType<typeof RemoteInstanceRecord>;
interface RemoteInstance {
host: string;
favicon: string | null;
federation: HostFederation;
}
const makeGetRemoteInstance = () =>
createSelector([
(_state: RootState, host: string) => host,
getRemoteInstanceFavicon,
getRemoteInstanceFederation,
], (host, favicon, federation) =>
RemoteInstanceRecord({
host,
favicon,
federation,
}));
], (host, favicon, federation): RemoteInstance => ({
host,
favicon,
federation,
}));
type ColumnQuery = { type: string; prefix?: string };
@ -347,7 +344,7 @@ const makeGetStatusIds = () => createSelector([
(state: RootState) => state.statuses,
], (columnSettings: any, statusIds: ImmutableOrderedSet<string>, statuses) =>
statusIds.filter((id: string) => {
const status = statuses.get(id);
const status = statuses[id];
if (!status) return true;
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 * as v from 'valibot';
import { mrfSimpleSchema } from 'pl-fe/schemas/pleroma';
type Config = ImmutableMap<string, any>;
import type { PleromaConfig } from 'pl-api';
type Policy = Record<string, any>;
type Config = PleromaConfig['configs'][0];
const find = (
configs: ImmutableList<Config>,
configs: PleromaConfig['configs'],
group: string,
key: string,
): 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 reducer = (acc: ImmutableMap<string, any>, curr: ImmutableMap<string, any>) => {
const key = curr.getIn(['tuple', 0]) as string;
const hosts = curr.getIn(['tuple', 1]) as ImmutableList<string>;
return acc.set(trimStart(key, ':'), ImmutableSet(hosts));
const reducer = (acc: Record<string, any>, curr: Record<string, any>) => {
const key = curr.tuple?.[0] as string;
const hosts = curr.tuple?.[1] as Array<string>;
return acc[trimStart(key, ':')] = hosts;
};
if (config?.get) {
const value = config.get('value', ImmutableList());
const result = value.reduce(reducer, ImmutableMap());
if (config) {
const value = config.value || [];
const result = value.reduce(reducer, {});
return v.parse(mrfSimpleSchema, result.toJS());
} else {
return v.parse(mrfSimpleSchema, {});
@ -38,7 +35,7 @@ const toSimplePolicy = (configs: ImmutableList<Config>) => {
};
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);