diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index 0fa26a7ab..03c636356 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -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, { diff --git a/packages/pl-fe/src/actions/accounts.ts b/packages/pl-fe/src/actions/accounts.ts index 770807208..61470d086 100644 --- a/packages/pl-fe/src/actions/accounts.ts +++ b/packages/pl-fe/src/actions/accounts.ts @@ -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) => ({ +const blockAccountSuccess = (relationship: Relationship, statuses: Record) => ({ type: ACCOUNT_BLOCK_SUCCESS, relationship, statuses, @@ -245,7 +244,7 @@ const muteAccountRequest = (accountId: string) => ({ accountId, }); -const muteAccountSuccess = (relationship: Relationship, statuses: ImmutableMap) => ({ +const muteAccountSuccess = (relationship: Relationship, statuses: Record) => ({ type: ACCOUNT_MUTE_SUCCESS, relationship, statuses, diff --git a/packages/pl-fe/src/actions/events.ts b/packages/pl-fe/src/actions/events.ts index 80fcba082..3a858ab26 100644 --- a/packages/pl-fe/src/actions/events.ts +++ b/packages/pl-fe/src/actions/events.ts @@ -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); diff --git a/packages/pl-fe/src/actions/moderation.tsx b/packages/pl-fe/src/actions/moderation.tsx index ddb490f2f..3ef1e0b91 100644 --- a/packages/pl-fe/src/actions/moderation.tsx +++ b/packages/pl-fe/src/actions/moderation.tsx @@ -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), diff --git a/packages/pl-fe/src/actions/mrf.ts b/packages/pl-fe/src/actions/mrf.ts index b5e4d2da6..6b16312d6 100644 --- a/packages/pl-fe/src/actions/mrf.ts +++ b/packages/pl-fe/src/actions/mrf.ts @@ -26,7 +26,7 @@ const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: const updateMrf = (host: string, restrictions: Record) => (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); diff --git a/packages/pl-fe/src/actions/pl-fe.ts b/packages/pl-fe/src/actions/pl-fe.ts index 2b2f4282d..80b0ea886 100644 --- a/packages/pl-fe/src/actions/pl-fe.ts +++ b/packages/pl-fe/src/actions/pl-fe.ts @@ -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); }); diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index b9a3e6547..3c12145a0 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -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 }); diff --git a/packages/pl-fe/src/actions/timelines.ts b/packages/pl-fe/src/actions/timelines.ts index 0b2a3a84f..8e83879ba 100644 --- a/packages/pl-fe/src/actions/timelines.ts +++ b/packages/pl-fe/src/actions/timelines.ts @@ -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; + references: Record; 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({ type: TIMELINE_DELETE, diff --git a/packages/pl-fe/src/components/helmet.tsx b/packages/pl-fe/src/components/helmet.tsx index 7823e21bd..dafab6605 100644 --- a/packages/pl-fe/src/components/helmet.tsx +++ b/packages/pl-fe/src/components/helmet.tsx @@ -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; }; diff --git a/packages/pl-fe/src/components/sidebar-navigation.tsx b/packages/pl-fe/src/components/sidebar-navigation.tsx index c910a4621..7ca5c403c 100644 --- a/packages/pl-fe/src/components/sidebar-navigation.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation.tsx @@ -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); diff --git a/packages/pl-fe/src/components/status-hover-card.tsx b/packages/pl-fe/src/components/status-hover-card.tsx index 380a5f478..1f7ad2eb9 100644 --- a/packages/pl-fe/src/components/status-hover-card.tsx +++ b/packages/pl-fe/src/components/status-hover-card.tsx @@ -24,7 +24,7 @@ const StatusHoverCard: React.FC = ({ 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) { diff --git a/packages/pl-fe/src/features/account-gallery/index.tsx b/packages/pl-fe/src/features/account-gallery/index.tsx index 27cfccc64..9a152e70e 100644 --- a/packages/pl-fe/src/features/account-gallery/index.tsx +++ b/packages/pl-fe/src/features/account-gallery/index.tsx @@ -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 = useAppSelector((state) => account ? getAccountGallery(state, account.id) : ImmutableList()); + const attachments: Array = 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 = ; } @@ -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 && (
@@ -116,7 +115,7 @@ const AccountGallery = () => { {loadOlder} - {isLoading && attachments.size === 0 && ( + {isLoading && attachments.length === 0 && (
diff --git a/packages/pl-fe/src/features/admin/components/admin-tabs.tsx b/packages/pl-fe/src/features/admin/components/admin-tabs.tsx index d7e9eaba5..90f7cad32 100644 --- a/packages/pl-fe/src/features/admin/components/admin-tabs.tsx +++ b/packages/pl-fe/src/features/admin/components/admin-tabs.tsx @@ -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', diff --git a/packages/pl-fe/src/features/admin/components/latest-accounts-panel.tsx b/packages/pl-fe/src/features/admin/components/latest-accounts-panel.tsx index f9a5f928f..dc50b53a8 100644 --- a/packages/pl-fe/src/features/admin/components/latest-accounts-panel.tsx +++ b/packages/pl-fe/src/features/admin/components/latest-accounts-panel.tsx @@ -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 = ({ limit = 5 }) => { const intl = useIntl(); const history = useHistory(); const dispatch = useAppDispatch(); - const accountIds = useAppSelector>((state) => state.admin.get('latestUsers').take(limit)); + const accountIds = useAppSelector>((state) => state.admin.latestUsers.slice(0, limit)); - const [total, setTotal] = useState(accountIds.size); + const [total, setTotal] = useState(accountIds.length); useEffect(() => { dispatch(fetchUsers({ @@ -46,7 +45,7 @@ const LatestAccountsPanel: React.FC = ({ limit = 5 }) => { onActionClick={handleAction} actionTitle={intl.formatMessage(messages.expand, { count: total })} > - {accountIds.take(limit).map((account) => ( + {accountIds.slice(0, limit).map((account) => ( ))} diff --git a/packages/pl-fe/src/features/admin/components/unapproved-account.tsx b/packages/pl-fe/src/features/admin/components/unapproved-account.tsx index 1e5b227b5..0373e2c27 100644 --- a/packages/pl-fe/src/features/admin/components/unapproved-account.tsx +++ b/packages/pl-fe/src/features/admin/components/unapproved-account.tsx @@ -18,7 +18,7 @@ const UnapprovedAccount: React.FC = ({ 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; diff --git a/packages/pl-fe/src/features/admin/tabs/awaiting-approval.tsx b/packages/pl-fe/src/features/admin/tabs/awaiting-approval.tsx index 0a17a57c4..a41097ba1 100644 --- a/packages/pl-fe/src/features/admin/tabs/awaiting-approval.tsx +++ b/packages/pl-fe/src/features/admin/tabs/awaiting-approval.tsx @@ -29,7 +29,7 @@ const AwaitingApproval: React.FC = () => { .catch(() => {}); }, []); - const showLoading = isLoading && accountIds.count() === 0; + const showLoading = isLoading && accountIds.length === 0; return ( { 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 ( void; + onSelectFile: (src: string) => void; } const UploadButton: React.FC = ({ onSelectFile }) => { diff --git a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx index c5df42cdb..ba5a410b9 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx @@ -40,7 +40,7 @@ const StatePlugin: React.FC = ({ 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; } diff --git a/packages/pl-fe/src/features/compose/editor/utils/get-selected-node.ts b/packages/pl-fe/src/features/compose/editor/utils/get-selected-node.ts index 2f093b983..f1fcd4f18 100644 --- a/packages/pl-fe/src/features/compose/editor/utils/get-selected-node.ts +++ b/packages/pl-fe/src/features/compose/editor/utils/get-selected-node.ts @@ -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(); diff --git a/packages/pl-fe/src/features/crypto-donate/utils/coin-db.ts b/packages/pl-fe/src/features/crypto-donate/utils/coin-db.ts index 8e31b7ed1..10d489aa0 100644 --- a/packages/pl-fe/src/features/crypto-donate/utils/coin-db.ts +++ b/packages/pl-fe/src/features/crypto-donate/utils/coin-db.ts @@ -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 : ''; }; diff --git a/packages/pl-fe/src/features/event/event-discussion.tsx b/packages/pl-fe/src/features/event/event-discussion.tsx index b104ba599..2d95d0401 100644 --- a/packages/pl-fe/src/features/event/event-discussion.tsx +++ b/packages/pl-fe/src/features/event/event-discussion.tsx @@ -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 = ({ 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(!!status); @@ -61,12 +60,12 @@ const EventDiscussion: React.FC = ({ 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 = ({ params: { statusId: statu ); }; - const renderChildren = (list: ImmutableOrderedSet) => list.map(id => { + const renderChildren = (list: Array) => list.map(id => { if (id.endsWith('-tombstone')) { return renderTombstone(id); } else if (id.startsWith('末pending-')) { @@ -120,7 +119,7 @@ const EventDiscussion: React.FC = ({ 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 = ({ params: { statusId: statu const children: JSX.Element[] = []; if (hasDescendants) { - children.push(...renderChildren(descendantsIds).toArray()); + children.push(...renderChildren(descendantsIds)); } return ( diff --git a/packages/pl-fe/src/features/event/event-information.tsx b/packages/pl-fe/src/features/event/event-information.tsx index 9c6554185..021aab416 100644 --- a/packages/pl-fe/src/features/event/event-information.tsx +++ b/packages/pl-fe/src/features/event/event-information.tsx @@ -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 = ({ 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(); diff --git a/packages/pl-fe/src/features/interaction-requests/index.tsx b/packages/pl-fe/src/features/interaction-requests/index.tsx index 9c2758ebc..57a16f2c6 100644 --- a/packages/pl-fe/src/features/interaction-requests/index.tsx +++ b/packages/pl-fe/src/features/interaction-requests/index.tsx @@ -52,7 +52,7 @@ interface IInteractionRequestStatus { } const InteractionRequestStatus: React.FC = ({ id: statusId, hasReply, isReply, actions }) => { - const status = useAppSelector((state) => state.statuses.get(statusId)); + const status = useAppSelector((state) => state.statuses[statusId]); if (!status) return null; diff --git a/packages/pl-fe/src/features/pl-fe-config/index.tsx b/packages/pl-fe/src/features/pl-fe-config/index.tsx index 006affe36..775f9fac6 100644 --- a/packages/pl-fe/src/features/pl-fe-config/index.tsx +++ b/packages/pl-fe/src/features/pl-fe-config/index.tsx @@ -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 = () => { e.target.value)} /> @@ -269,7 +270,7 @@ const PlFeConfig: React.FC = () => { e.target.value)} /> diff --git a/packages/pl-fe/src/features/status/components/thread-status.tsx b/packages/pl-fe/src/features/status/components/thread-status.tsx index ecd7bb12f..4a14aed3b 100644 --- a/packages/pl-fe/src/features/status/components/thread-status.tsx +++ b/packages/pl-fe/src/features/status/components/thread-status.tsx @@ -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 = (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; diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index 034e0c71b..49febcfde 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -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(); + let ancestorsIds: Array = []; 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(); + let descendantsIds: Array = []; 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 = ({ 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(null); const statusRef = useRef(null); @@ -213,13 +212,13 @@ const Thread: React.FC = ({ 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 = ({ 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 = ({ ); }; - const renderChildren = (list: ImmutableOrderedSet) => list.map(id => { + const renderChildren = (list: Array) => list.map(id => { if (id.endsWith('-tombstone')) { return renderTombstone(id); } else if (id.startsWith('末pending-')) { @@ -301,8 +300,8 @@ const Thread: React.FC = ({ // 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) => { openModal('COMPARE_HISTORY', { @@ -310,8 +309,8 @@ const Thread: React.FC = ({ }); }; - 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 = ({ } 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 ( diff --git a/packages/pl-fe/src/features/ui/components/modals/compare-history-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/compare-history-modal.tsx index 8eee63028..a24991112 100644 --- a/packages/pl-fe/src/features/ui/components/modals/compare-history-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/compare-history-modal.tsx @@ -25,7 +25,7 @@ const CompareHistoryModal: React.FC = 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'); diff --git a/packages/pl-fe/src/features/ui/components/modals/report-modal/components/status-check-box.tsx b/packages/pl-fe/src/features/ui/components/modals/report-modal/components/status-check-box.tsx index 588213f29..cd8a9231f 100644 --- a/packages/pl-fe/src/features/ui/components/modals/report-modal/components/status-check-box.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/report-modal/components/status-check-box.tsx @@ -15,7 +15,7 @@ interface IStatusCheckBox { } const StatusCheckBox: React.FC = ({ id, disabled, checked, toggleStatusReport }) => { - const status = useAppSelector((state) => state.statuses.get(id)); + const status = useAppSelector((state) => state.statuses[id]); const onToggle: React.ChangeEventHandler = (e) => toggleStatusReport(e.target.checked); diff --git a/packages/pl-fe/src/features/ui/components/modals/report-modal/index.tsx b/packages/pl-fe/src/features/ui/components/modals/report-modal/index.tsx index 5d306d630..f0d22645e 100644 --- a/packages/pl-fe/src/features/ui/components/modals/report-modal/index.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/report-modal/index.tsx @@ -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; diff --git a/packages/pl-fe/src/features/ui/components/modals/select-bookmark-folder-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/select-bookmark-folder-modal.tsx index 93b34c39e..6c93d49fa 100644 --- a/packages/pl-fe/src/features/ui/components/modals/select-bookmark-folder-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/select-bookmark-folder-modal.tsx @@ -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 = ({ 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); diff --git a/packages/pl-fe/src/features/ui/components/panels/group-media-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/group-media-panel.tsx index d7b97faf4..b9e1e7066 100644 --- a/packages/pl-fe/src/features/ui/components/panels/group-media-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/group-media-panel.tsx @@ -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 = ({ group }) => { const isMember = !!group?.relationship?.member; const isPrivate = group?.locked; - const attachments: ImmutableList = useAppSelector((state) => group ? getGroupGallery(state, group?.id) : ImmutableList()); + const attachments: Array = useAppSelector((state) => group ? getGroupGallery(state, group?.id) : []); const handleOpenMedia = (attachment: AccountGalleryAttachment): void => { if (attachment.type === 'video') { @@ -55,7 +54,7 @@ const GroupMediaPanel: React.FC = ({ group }) => { const renderAttachments = () => { const nineAttachments = attachments.slice(0, 9); - if (!nineAttachments.isEmpty()) { + if (nineAttachments.length) { return (
{nineAttachments.map((attachment, index) => ( @@ -63,7 +62,7 @@ const GroupMediaPanel: React.FC = ({ group }) => { key={`${attachment.status.id}+${attachment.id}`} attachment={attachment} onOpenMedia={handleOpenMedia} - isLast={index === nineAttachments.size - 1} + isLast={index === nineAttachments.length - 1} /> ))}
diff --git a/packages/pl-fe/src/features/ui/components/panels/profile-media-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/profile-media-panel.tsx index c51ecc04e..abe17921c 100644 --- a/packages/pl-fe/src/features/ui/components/panels/profile-media-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/profile-media-panel.tsx @@ -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 = ({ account }) => { const [loading, setLoading] = useState(true); - const attachments: ImmutableList = useAppSelector((state) => account ? getAccountGallery(state, account?.id) : ImmutableList()); + const attachments: Array = useAppSelector((state) => account ? getAccountGallery(state, account?.id) : []); const handleOpenMedia = (attachment: AccountGalleryAttachment): void => { if (attachment.type === 'video') { @@ -53,7 +52,7 @@ const ProfileMediaPanel: React.FC = ({ account }) => { const publicAttachments = attachments.filter(attachment => attachment.status.visibility === 'public'); const nineAttachments = publicAttachments.slice(0, 9); - if (!nineAttachments.isEmpty()) { + if (nineAttachments.length) { return (
{nineAttachments.map((attachment, index) => ( @@ -61,7 +60,7 @@ const ProfileMediaPanel: React.FC = ({ account }) => { key={`${attachment.status.id}+${attachment.id}`} attachment={attachment} onOpenMedia={handleOpenMedia} - isLast={index === nineAttachments.size - 1} + isLast={index === nineAttachments.length - 1} /> ))}
diff --git a/packages/pl-fe/src/features/ui/util/pending-status-builder.ts b/packages/pl-fe/src/features/ui/util/pending-status-builder.ts index 9143e67a5..0c3cbc325 100644 --- a/packages/pl-fe/src/features/ui/util/pending-status-builder.ts +++ b/packages/pl-fe/src/features/ui/util/pending-status-builder.ts @@ -37,12 +37,12 @@ const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotency account, content: pendingStatus.status.replace(new RegExp('\n', 'g'), '
'), /* 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, }; diff --git a/packages/pl-fe/src/reducers/admin.ts b/packages/pl-fe/src/reducers/admin.ts index aa23ae2a6..7531f4541 100644 --- a/packages/pl-fe/src/reducers/admin.ts +++ b/packages/pl-fe/src/reducers/admin.ts @@ -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(), - openReports: ImmutableOrderedSet(), - users: ImmutableMap(), - latestUsers: ImmutableOrderedSet(), - awaitingApproval: ImmutableOrderedSet(), - configs: ImmutableList(), +interface State { + reports: Record; + openReports: Array; + users: Record; + latestUsers: Array; + awaitingApproval: Array; + configs: Array; + needsReboot: boolean; +} + +const initialState: State = { + reports: {}, + openReports: [], + users: {}, + latestUsers: [], + awaitingApproval: [], + configs: [], needsReboot: false, -}); - -type State = ReturnType; - -// Lol https://javascript.plainenglish.io/typescript-essentials-conditionally-filter-types-488705bfbf56 -type FilterConditionally = Pick; - -type SetKeys = keyof FilterConditionally>; +}; const toIds = (items: any[]) => items.map(item => item.id); -const mergeSet = (state: State, key: SetKeys, users: Array): State => { - const newIds = toIds(users); - return state.update(key, (ids: ImmutableOrderedSet) => ids.union(newIds)); -}; - -const replaceSet = (state: State, key: SetKeys, users: Array): State => { - const newIds = toIds(users); - return state.set(key, ImmutableOrderedSet(newIds)); -}; - -const maybeImportUnapproved = (state: State, users: Array, params?: AdminGetAccountsParams): State => { +const maybeImportUnapproved = (state: State, users: Array, 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, params?: AdminGetAccountsParams): State => { +const maybeImportLatest = (state: State, users: Array, 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; -const importUsers = (state: State, users: Array, params: AdminGetAccountsParams): State => - state.withMutations(state => { - maybeImportUnapproved(state, users, params); - maybeImportLatest(state, users, params); +const importUsers = (state: State, users: Array, 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; -const importReports = (state: State, reports: Array): 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) => { + 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 => 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; } diff --git a/packages/pl-fe/src/reducers/contexts.ts b/packages/pl-fe/src/reducers/contexts.ts index 104421868..a61a7cd1f 100644 --- a/packages/pl-fe/src/reducers/contexts.ts +++ b/packages/pl-fe/src/reducers/contexts.ts @@ -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(), - replies: ImmutableMap>(), -}); +interface State { + inReplyTos: Record; + replies: Record>; +} -type State = ReturnType; +const initialState: State = { + inReplyTos: {}, + replies: {}, +}; /** Import a single status into the reducer, setting replies and replyTos. */ -const importStatus = (state: State, status: Pick, idempotencyKey?: string): State => { +const importStatus = (state: State, status: Pick, 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>): State => - state.withMutations(state => { - statuses.forEach(status => importStatus(state, status)); - }); +const importStatuses = (state: State, statuses: Array>) => + 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>, 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>, 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>, descendants: Array>, -) => 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, -): 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, idempotencyKey: string): State => { +const importPendingStatus = (state: State, params: Pick, 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, idempotencyKey: string): State => { +const deletePendingStatus = (state: State, params: Pick, 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, }; diff --git a/packages/pl-fe/src/reducers/instance.ts b/packages/pl-fe/src/reducers/instance.ts index b7b4144e9..8dfd42ebd 100644 --- a/packages/pl-fe/src/reducers/instance.ts +++ b/packages/pl-fe/src/reducers/instance.ts @@ -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, key: string) return v ? v.getIn(['tuple', 1]) : undefined; }; -const importConfigs = (state: State, configs: ImmutableList) => { +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) => { 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; } diff --git a/packages/pl-fe/src/reducers/pl-fe.ts b/packages/pl-fe/src/reducers/pl-fe.ts index 509a560ef..f04fcf020 100644 --- a/packages/pl-fe/src/reducers/pl-fe.ts +++ b/packages/pl-fe/src/reducers/pl-fe.ts @@ -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(); +import type { PleromaConfig } from 'pl-api'; -const fallbackState = ImmutableMap({ +const initialState: Record = {}; + +const fallbackState = { brandColor: '#d80482', -}); +}; -const updateFromAdmin = (state: ImmutableMap, configs: ImmutableList>) => { +const updateFromAdmin = (state: Record, configs: PleromaConfig['configs']) => { try { return ConfigDB.find(configs, ':pleroma', ':frontend_configurations')! - .get('value') - .find((value: ImmutableMap) => value.getIn(['tuple', 0]) === ':pl_fe') - .getIn(['tuple', 1]); + .value + .find((value: Record) => value.tuple?.[0] === ':pl_fe') + .tuple?.[1]; } catch { return state; } }; -const preloadImport = (state: ImmutableMap, action: Record) => { +const preloadImport = (state: Record, action: Record) => { 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, host: string) => { +const persistPlFeConfig = (plFeConfig: Record, 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, host: string) => { +const importPlFeConfig = (plFeConfig: Record, host: string) => { persistPlFeConfig(plFeConfig, host); return plFeConfig; }; -const plfe = (state = initialState, action: Record) => { +const plfe = (state = initialState, action: Record): Record => { 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, 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>); + return updateFromAdmin(state, action.configs || []); default: return state; } diff --git a/packages/pl-fe/src/reducers/statuses.ts b/packages/pl-fe/src/reducers/statuses.ts index 478b9d35c..cd5e28227 100644 --- a/packages/pl-fe/src/reducers/statuses.ts +++ b/packages/pl-fe/src/reducers/statuses.ts @@ -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; +type State = Record; type MinifiedStatus = ReturnType; @@ -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): State => - state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status))); +const importStatuses = (state: State, statuses: Array) =>{ + statuses.forEach(status => importStatus(state, status)); +}; const deleteStatus = (state: State, statusId: string, references: Array) => { 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; } diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index 5043b296a..a1ba3374d 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -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(), (state: RootState) => state.statuses, ], (statusIds, statuses) => - statusIds.reduce((medias: ImmutableList, statusId: string) => { - const status = statuses.get(statusId); + statusIds.reduce((medias: Array, 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(), (state: RootState) => state.statuses, ], (statusIds, statuses) => - statusIds.reduce((medias: ImmutableList, statusId: string) => { - const status = statuses.get(statusId); + statusIds.reduce((medias: Array, 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; 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; +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, statuses) => statusIds.filter((id: string) => { - const status = statuses.get(id); + const status = statuses[id]; if (!status) return true; return !shouldFilter(status, columnSettings); }), diff --git a/packages/pl-fe/src/utils/config-db.ts b/packages/pl-fe/src/utils/config-db.ts index bd39fcd99..7ed0b256f 100644 --- a/packages/pl-fe/src/utils/config-db.ts +++ b/packages/pl-fe/src/utils/config-db.ts @@ -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; +import type { PleromaConfig } from 'pl-api'; + type Policy = Record; +type Config = PleromaConfig['configs'][0]; const find = ( - configs: ImmutableList, + 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) => { +const toSimplePolicy = (configs: PleromaConfig['configs']) => { const config = find(configs, ':pleroma', ':mrf_simple'); - const reducer = (acc: ImmutableMap, curr: ImmutableMap) => { - const key = curr.getIn(['tuple', 0]) as string; - const hosts = curr.getIn(['tuple', 1]) as ImmutableList; - return acc.set(trimStart(key, ':'), ImmutableSet(hosts)); + const reducer = (acc: Record, curr: Record) => { + const key = curr.tuple?.[0] as string; + const hosts = curr.tuple?.[1] as Array; + 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) => { }; const fromSimplePolicy = (simplePolicy: Policy) => { - const mapper = ([key, hosts]: [key: string, hosts: ImmutableList]) => ({ tuple: [`:${key}`, hosts] }); + const mapper = ([key, hosts]: [key: string, hosts: Array]) => ({ tuple: [`:${key}`, hosts] }); const value = Object.entries(simplePolicy).map(mapper);