From 5c58196201401fb1dca534f73f1923a30925837c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 10 Nov 2024 00:05:08 +0100 Subject: [PATCH] pl-fe: migrate drafts to mutative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- packages/pl-fe/src/actions/draft-statuses.ts | 39 ++++++++-- .../pl-fe/src/components/sidebar-menu.tsx | 2 +- .../src/components/sidebar-navigation.tsx | 2 +- .../src/features/draft-statuses/builder.tsx | 6 +- .../src/features/draft-statuses/index.tsx | 2 +- packages/pl-fe/src/reducers/draft-statuses.ts | 71 ++++++++++--------- 6 files changed, 74 insertions(+), 48 deletions(-) diff --git a/packages/pl-fe/src/actions/draft-statuses.ts b/packages/pl-fe/src/actions/draft-statuses.ts index a4f786765..1947bbb1f 100644 --- a/packages/pl-fe/src/actions/draft-statuses.ts +++ b/packages/pl-fe/src/actions/draft-statuses.ts @@ -3,27 +3,40 @@ import { makeGetAccount } from 'pl-fe/selectors'; import KVStore from 'pl-fe/storage/kv-store'; import type { AppDispatch, RootState } from 'pl-fe/store'; +import type { APIEntity } from 'pl-fe/types/entities'; -const DRAFT_STATUSES_FETCH_SUCCESS = 'DRAFT_STATUSES_FETCH_SUCCESS'; -const PERSIST_DRAFT_STATUS = 'PERSIST_DRAFT_STATUS'; -const CANCEL_DRAFT_STATUS = 'DELETE_DRAFT_STATUS'; +const DRAFT_STATUSES_FETCH_SUCCESS = 'DRAFT_STATUSES_FETCH_SUCCESS' as const; + +const PERSIST_DRAFT_STATUS = 'PERSIST_DRAFT_STATUS' as const; +const CANCEL_DRAFT_STATUS = 'DELETE_DRAFT_STATUS' as const; const getAccount = makeGetAccount(); +interface DraftStatusesFetchSuccessAction { + type: typeof DRAFT_STATUSES_FETCH_SUCCESS; + statuses: Array; +} + const fetchDraftStatuses = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const accountUrl = getAccount(state, state.me as string)!.url; - return KVStore.getItem(`drafts:${accountUrl}`).then((statuses) => { - dispatch({ + return KVStore.getItem>(`drafts:${accountUrl}`).then((statuses) => { + dispatch({ type: DRAFT_STATUSES_FETCH_SUCCESS, statuses, }); }).catch(() => {}); }; +interface PersistDraftStatusAction { + type: typeof PERSIST_DRAFT_STATUS; + status: Record; + accountUrl: string; +} + const saveDraftStatus = (composeId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -36,25 +49,36 @@ const saveDraftStatus = (composeId: string) => draft_id: compose.draft_id || crypto.randomUUID(), }; - dispatch({ + dispatch({ type: PERSIST_DRAFT_STATUS, status: draft, accountUrl, }); }; +interface CancelDraftStatusAction { + type: typeof CANCEL_DRAFT_STATUS; + statusId: string; + accountUrl: string; +} + const cancelDraftStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const accountUrl = getAccount(state, state.me as string)!.url; - dispatch({ + dispatch({ type: CANCEL_DRAFT_STATUS, statusId, accountUrl, }); }; +type DraftStatusesAction = + | DraftStatusesFetchSuccessAction + | PersistDraftStatusAction + | CancelDraftStatusAction + export { DRAFT_STATUSES_FETCH_SUCCESS, PERSIST_DRAFT_STATUS, @@ -62,4 +86,5 @@ export { fetchDraftStatuses, saveDraftStatus, cancelDraftStatus, + type DraftStatusesAction, }; diff --git a/packages/pl-fe/src/components/sidebar-menu.tsx b/packages/pl-fe/src/components/sidebar-menu.tsx index ca4129b89..2ea086a02 100644 --- a/packages/pl-fe/src/components/sidebar-menu.tsx +++ b/packages/pl-fe/src/components/sidebar-menu.tsx @@ -99,7 +99,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.length); const interactionRequestsCount = useInteractionRequestsCount().data || 0; const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); - const draftCount = useAppSelector((state) => state.draft_statuses.size); + const draftCount = useAppSelector((state) => Object.keys(state.draft_statuses).length); // const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); const [sidebarVisible, setSidebarVisible] = useState(isSidebarOpen); const touchStart = useRef(0); diff --git a/packages/pl-fe/src/components/sidebar-navigation.tsx b/packages/pl-fe/src/components/sidebar-navigation.tsx index 8b3c97739..33f4748ff 100644 --- a/packages/pl-fe/src/components/sidebar-navigation.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation.tsx @@ -52,7 +52,7 @@ const SidebarNavigation = () => { const interactionRequestsCount = useInteractionRequestsCount().data || 0; 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); + const draftCount = useAppSelector((state) => Object.keys(state.draft_statuses).length); const restrictUnauth = instance.pleroma.metadata.restrict_unauthenticated; diff --git a/packages/pl-fe/src/features/draft-statuses/builder.tsx b/packages/pl-fe/src/features/draft-statuses/builder.tsx index c456e39b4..beb05ec3f 100644 --- a/packages/pl-fe/src/features/draft-statuses/builder.tsx +++ b/packages/pl-fe/src/features/draft-statuses/builder.tsx @@ -8,11 +8,11 @@ import type { DraftStatus } from 'pl-fe/reducers/draft-statuses'; import type { RootState } from 'pl-fe/store'; const buildPoll = (draftStatus: DraftStatus) => { - if (draftStatus.hasIn(['poll', 'options'])) { + if (draftStatus.poll?.options) { return { - ...draftStatus.poll!.toJS(), + ...draftStatus.poll, id: `${draftStatus.draft_id}-poll`, - options: draftStatus.poll!.get('options').map((title: string) => ({ title })).toArray(), + options: draftStatus.poll.options.map((title: string) => ({ title })).toArray(), }; } else { return null; diff --git a/packages/pl-fe/src/features/draft-statuses/index.tsx b/packages/pl-fe/src/features/draft-statuses/index.tsx index 8d405abee..d7566d41c 100644 --- a/packages/pl-fe/src/features/draft-statuses/index.tsx +++ b/packages/pl-fe/src/features/draft-statuses/index.tsx @@ -31,7 +31,7 @@ const DraftStatuses = () => { emptyMessage={emptyMessage} listClassName='divide-y divide-solid divide-gray-200 dark:divide-gray-800' > - {drafts.toOrderedSet().reverse().map((draft) => )} + {Object.values(drafts).toReversed().map((draft) => )} ); diff --git a/packages/pl-fe/src/reducers/draft-statuses.ts b/packages/pl-fe/src/reducers/draft-statuses.ts index 76fd87b90..7df0ffbdc 100644 --- a/packages/pl-fe/src/reducers/draft-statuses.ts +++ b/packages/pl-fe/src/reducers/draft-statuses.ts @@ -1,63 +1,64 @@ -import { List as ImmutableList, Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable'; +import { create } from 'mutative'; +import { mediaAttachmentSchema } from 'pl-api'; +import * as v from 'valibot'; import { COMPOSE_SUBMIT_SUCCESS, type ComposeAction } from 'pl-fe/actions/compose'; -import { DRAFT_STATUSES_FETCH_SUCCESS, PERSIST_DRAFT_STATUS, CANCEL_DRAFT_STATUS } from 'pl-fe/actions/draft-statuses'; +import { DRAFT_STATUSES_FETCH_SUCCESS, PERSIST_DRAFT_STATUS, CANCEL_DRAFT_STATUS, DraftStatusesAction } from 'pl-fe/actions/draft-statuses'; +import { filteredArray } from 'pl-fe/schemas/utils'; import KVStore from 'pl-fe/storage/kv-store'; -import type { MediaAttachment } from 'pl-api'; -import type { StatusVisibility } from 'pl-fe/normalizers/status'; import type { APIEntity } from 'pl-fe/types/entities'; -import type { AnyAction } from 'redux'; -const DraftStatusRecord = ImmutableRecord({ - content_type: 'text/plain', - draft_id: '', - editorState: null as string | null, - group_id: null as string | null, - in_reply_to: null as string | null, - media_attachments: ImmutableList(), - poll: null as ImmutableMap | null, - privacy: 'public' as StatusVisibility, - quote: null as string | null, - schedule: null as Date | null, - sensitive: false, - spoiler: false, - spoiler_text: '', - text: '', - to: ImmutableOrderedSet(), +const draftStatusSchema = v.object({ + content_type: v.fallback(v.string(), 'text/plain'), + draft_id: v.string(), + editorState: v.fallback(v.nullable(v.string()), null), + group_id: v.fallback(v.nullable(v.string()), null), + in_reply_to: v.fallback(v.nullable(v.string()), null), + media_attachments: filteredArray(mediaAttachmentSchema), + poll: v.fallback(v.nullable(v.record(v.string(), v.any())), null), + privacy: v.fallback(v.string(), 'public'), + quote: v.fallback(v.nullable(v.string()), null), + schedule: v.fallback(v.nullable(v.string()), null), + sensitive: v.fallback(v.boolean(), false), + spoiler: v.fallback(v.boolean(), false), + spoiler_text: v.fallback(v.string(), ''), + text: v.fallback(v.string(), ''), }); -type DraftStatus = ReturnType; -type State = ImmutableMap; +type DraftStatus = v.InferOutput; +type State = Record; -const initialState: State = ImmutableMap(); +const initialState: State = {}; -const importStatus = (state: State, status: APIEntity) => - state.set(status.draft_id, DraftStatusRecord(ImmutableMap(fromJS(status)))); +const importStatus = (state: State, status: APIEntity) => { + state[status.draft_id] = v.parse(draftStatusSchema, status); +}; -const importStatuses = (state: State, statuses: APIEntity[]) => - state.withMutations(mutable => Object.values(statuses || {}).forEach(status => importStatus(mutable, status))); +const importStatuses = (state: State, statuses: APIEntity[]) => { + Object.values(statuses || {}).forEach(status => importStatus(state, status)); +}; const deleteStatus = (state: State, statusId: string) => { - if (statusId) return state.delete(statusId); + if (statusId) delete state[statusId]; return state; }; const persistState = (state: State, accountUrl: string) => { - KVStore.setItem(`drafts:${accountUrl}`, state.toJS()); + KVStore.setItem(`drafts:${accountUrl}`, state); return state; }; -const scheduled_statuses = (state: State = initialState, action: AnyAction | ComposeAction) => { +const scheduled_statuses = (state: State = initialState, action: DraftStatusesAction | ComposeAction) => { switch (action.type) { case DRAFT_STATUSES_FETCH_SUCCESS: - return importStatuses(state, action.statuses); + return create(state, (draft) => importStatuses(draft, action.statuses)); case PERSIST_DRAFT_STATUS: - return persistState(importStatus(state, action.status), action.accountUrl); + return persistState(create(state, (draft) => importStatus(draft, action.status)), action.accountUrl); case CANCEL_DRAFT_STATUS: - return persistState(deleteStatus(state, action.statusId), action.accountUrl); + return persistState(create(state, (draft) => deleteStatus(draft, action.statusId)), action.accountUrl); case COMPOSE_SUBMIT_SUCCESS: - return persistState(deleteStatus(state, action.draftId), action.accountUrl); + return action.draftId ? persistState(create(state, (draft) => deleteStatus(draft, action.draftId!)), action.accountUrl) : state; default: return state; }