From f1aea5f17ecacbcc8987c2eeabf3c02337272fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 22 Feb 2026 20:36:05 +0100 Subject: [PATCH] nicolium: group migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/importer.ts | 7 +- .../src/api/hooks/groups/use-create-group.ts | 21 --- .../src/api/hooks/groups/use-delete-group.ts | 18 -- .../src/api/hooks/groups/use-update-group.ts | 28 ---- .../src/components/status-action-bar.tsx | 19 +-- packages/pl-fe/src/components/status.tsx | 3 +- packages/pl-fe/src/entity-store/actions.ts | 65 +------- packages/pl-fe/src/entity-store/entities.ts | 8 +- .../pl-fe/src/entity-store/hooks/types.ts | 27 +-- .../hooks/use-batched-entities.ts | 107 ------------ .../entity-store/hooks/use-create-entity.ts | 59 ------- .../entity-store/hooks/use-delete-entity.ts | 57 ------- .../entity-store/hooks/use-dismiss-entity.ts | 33 ---- .../src/entity-store/hooks/use-entities.ts | 154 ------------------ .../pl-fe/src/entity-store/hooks/utils.ts | 15 -- packages/pl-fe/src/entity-store/reducer.ts | 96 ----------- packages/pl-fe/src/entity-store/selectors.ts | 53 +----- .../components/group-member-list-item.tsx | 5 - .../status/components/detailed-status.tsx | 53 +++--- .../src/modals/manage-group-modal/index.tsx | 13 +- packages/pl-fe/src/normalizers/status.ts | 4 - .../pl-fe/src/pages/groups/edit-group.tsx | 27 ++- .../pl-fe/src/pages/groups/manage-group.tsx | 7 +- .../pl-fe/src/queries/groups/use-group.ts | 48 +++++- 24 files changed, 121 insertions(+), 806 deletions(-) delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-create-group.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-delete-group.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-update-group.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/use-batched-entities.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/use-create-entity.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/use-delete-entity.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/use-dismiss-entity.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/use-entities.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/utils.ts diff --git a/packages/pl-fe/src/actions/importer.ts b/packages/pl-fe/src/actions/importer.ts index e9c9e0a1e..e0a8251df 100644 --- a/packages/pl-fe/src/actions/importer.ts +++ b/packages/pl-fe/src/actions/importer.ts @@ -109,7 +109,12 @@ const importEntities = if (!isEmpty(accounts)) dispatch(importEntityStoreEntities(Object.values(accounts), Entities.ACCOUNTS)); if (!isEmpty(groups)) - dispatch(importEntityStoreEntities(Object.values(groups), Entities.GROUPS)); + for (const group of Object.values(groups)) { + queryClient.setQueryData(['groups', group.id], group); + if (group.relationship) { + queryClient.setQueryData(['groupRelationships', group.id], group.relationship); + } + } if (!isEmpty(polls)) { for (const poll of Object.values(polls)) { queryClient.setQueryData(['statuses', 'polls', poll.id], poll); diff --git a/packages/pl-fe/src/api/hooks/groups/use-create-group.ts b/packages/pl-fe/src/api/hooks/groups/use-create-group.ts deleted file mode 100644 index 61d22d580..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-create-group.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Entities } from '@/entity-store/entities'; -import { useCreateEntity } from '@/entity-store/hooks/use-create-entity'; -import { useClient } from '@/hooks/use-client'; - -import type { Group, CreateGroupParams } from 'pl-api'; - -const useCreateGroup = () => { - const client = useClient(); - - const { createEntity, ...rest } = useCreateEntity( - [Entities.GROUPS, 'search', ''], - (params: CreateGroupParams) => client.experimental.groups.createGroup(params), - ); - - return { - createGroup: createEntity, - ...rest, - }; -}; - -export { useCreateGroup }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-delete-group.ts b/packages/pl-fe/src/api/hooks/groups/use-delete-group.ts deleted file mode 100644 index 3961eaea0..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-delete-group.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Entities } from '@/entity-store/entities'; -import { useDeleteEntity } from '@/entity-store/hooks/use-delete-entity'; -import { useClient } from '@/hooks/use-client'; - -const useDeleteGroup = () => { - const client = useClient(); - - const { deleteEntity, isSubmitting } = useDeleteEntity(Entities.GROUPS, (groupId: string) => - client.experimental.groups.deleteGroup(groupId), - ); - - return { - mutate: deleteEntity, - isSubmitting, - }; -}; - -export { useDeleteGroup }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-update-group.ts b/packages/pl-fe/src/api/hooks/groups/use-update-group.ts deleted file mode 100644 index dcf86e2e1..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-update-group.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Entities } from '@/entity-store/entities'; -import { useCreateEntity } from '@/entity-store/hooks/use-create-entity'; -import { useClient } from '@/hooks/use-client'; - -interface UpdateGroupParams { - display_name?: string; - note?: string; - avatar?: File | ''; - header?: File | ''; - group_visibility?: string; - discoverable?: boolean; -} - -const useUpdateGroup = (groupId: string) => { - const client = useClient(); - - const { createEntity, ...rest } = useCreateEntity( - [Entities.GROUPS], - (params: UpdateGroupParams) => client.experimental.groups.updateGroup(groupId, params), - ); - - return { - updateGroup: createEntity, - ...rest, - }; -}; - -export { useUpdateGroup }; diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 932bdc561..b753bf519 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -1,5 +1,5 @@ import { useMatch, useNavigate } from '@tanstack/react-router'; -import { type Account, type CustomEmoji, type Group, GroupRoles } from 'pl-api'; +import { type Account, type CustomEmoji, GroupRoles } from 'pl-api'; import React, { useCallback, useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -26,7 +26,6 @@ import { useUnblockAccountMutation } from '@/queries/accounts/use-relationship'; import { useChats } from '@/queries/chats'; import { useGroupQuery } from '@/queries/groups/use-group'; import { useBlockGroupUserMutation } from '@/queries/groups/use-group-blocks'; -import { useGroupRelationshipQuery } from '@/queries/groups/use-group-relationship'; import { useCustomEmojis } from '@/queries/instance/use-custom-emojis'; import { useTranslationLanguages } from '@/queries/instance/use-translation-languages'; import { @@ -345,12 +344,12 @@ const ReplyButton: React.FC = ({ const intl = useIntl(); const canReply = useCanInteract(status, 'can_reply'); - const { data: groupRelationship } = useGroupRelationshipQuery(status.group_id ?? undefined); + const { data: group } = useGroupQuery(status.group_id ?? undefined, true); let replyTitle; let replyDisabled = false; - if ((status.group as Group)?.membership_required && !groupRelationship?.member) { + if (group?.membership_required && !group.relationship?.member) { replyDisabled = true; replyTitle = intl.formatMessage(messages.replies_disabled_group); } @@ -394,8 +393,8 @@ const ReplyButton: React.FC = ({ ); - return status.group ? ( - + return group ? ( + {replyButton} ) : ( @@ -741,8 +740,8 @@ const MenuButton: React.FC = ({ const { openModal } = useModalsActions(); const { data: group } = useGroupQuery(status.group_id || undefined, true); const { mutate: blockGroupMember } = useBlockGroupUserMutation( - status.group?.id as string, - status.account.id, + status.group_id as string, + status.account_id, ); const { getOrCreateChatByAccountId } = useChats(); const { mutate: bookmarkStatus } = useBookmarkStatus(status.id); @@ -1032,7 +1031,7 @@ const MenuButton: React.FC = ({ }); } - const isGroupStatus = typeof status.group === 'object'; + const isGroupStatus = typeof status.group_id === 'string'; if (features.bookmarks) { menu.push({ @@ -1227,7 +1226,7 @@ const MenuButton: React.FC = ({ }); } - if (isGroupStatus && !!status.group) { + if (isGroupStatus && !!status.group_id) { const isGroupOwner = group?.relationship?.role === GroupRoles.OWNER; const isGroupAdmin = group?.relationship?.role === GroupRoles.ADMIN; // const isStatusFromOwner = group.owner.id === account.id; diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index da7330e32..05fa45847 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -205,13 +205,14 @@ const Status: React.FC = (props) => { (state) => (status.reblog_id && getStatus(state, { id: status.reblog_id })!) || status, ); + const { data: group } = useGroupQuery(actualStatus.group_id ?? undefined); + const { mutate: favouriteStatus } = useFavouriteStatus(actualStatus.id); const { mutate: unfavouriteStatus } = useUnfavouriteStatus(actualStatus.id); const { mutate: reblogStatus } = useReblogStatus(actualStatus.id); const { mutate: unreblogStatus } = useUnreblogStatus(actualStatus.id); const isReblog = status.reblog_id; - const group = actualStatus.group; const filterResults = useMemo(() => { return [...status.filtered, ...actualStatus.filtered] diff --git a/packages/pl-fe/src/entity-store/actions.ts b/packages/pl-fe/src/entity-store/actions.ts index b2de9cc75..5b4f71397 100644 --- a/packages/pl-fe/src/entity-store/actions.ts +++ b/packages/pl-fe/src/entity-store/actions.ts @@ -1,13 +1,8 @@ import type { Entities } from './entities'; -import type { EntitiesTransaction, Entity, EntityListState, ImportPosition } from './types'; +import type { EntitiesTransaction, Entity, ImportPosition } from './types'; const ENTITIES_IMPORT = 'ENTITIES_IMPORT' as const; const ENTITIES_DELETE = 'ENTITIES_DELETE' as const; -const ENTITIES_DISMISS = 'ENTITIES_DISMISS' as const; -const ENTITIES_FETCH_REQUEST = 'ENTITIES_FETCH_REQUEST' as const; -const ENTITIES_FETCH_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const; -const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const; -const ENTITIES_INVALIDATE_LIST = 'ENTITIES_INVALIDATE_LIST' as const; const ENTITIES_TRANSACTION = 'ENTITIES_TRANSACTION' as const; /** Action to import entities into the cache. */ @@ -39,49 +34,6 @@ const deleteEntities = ( opts, }); -const dismissEntities = (ids: Iterable, entityType: string, listKey: string) => ({ - type: ENTITIES_DISMISS, - ids, - entityType, - listKey, -}); - -const entitiesFetchRequest = (entityType: string, listKey?: string) => ({ - type: ENTITIES_FETCH_REQUEST, - entityType, - listKey, -}); - -const entitiesFetchSuccess = ( - entities: Entity[], - entityType: Entities, - listKey?: string, - pos?: ImportPosition, - newState?: EntityListState, - overwrite = false, -) => ({ - type: ENTITIES_FETCH_SUCCESS, - entityType, - entities, - listKey, - pos, - newState, - overwrite, -}); - -const entitiesFetchFail = (entityType: Entities, listKey: string | undefined, error: any) => ({ - type: ENTITIES_FETCH_FAIL, - entityType, - listKey, - error, -}); - -const invalidateEntityList = (entityType: Entities, listKey: string) => ({ - type: ENTITIES_INVALIDATE_LIST, - entityType, - listKey, -}); - const entitiesTransaction = (transaction: EntitiesTransaction) => ({ type: ENTITIES_TRANSACTION, transaction, @@ -91,11 +43,6 @@ const entitiesTransaction = (transaction: EntitiesTransaction) => ({ type EntityAction = | ReturnType | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType | ReturnType; export { @@ -103,18 +50,8 @@ export { type EntityAction, ENTITIES_IMPORT, ENTITIES_DELETE, - ENTITIES_DISMISS, - ENTITIES_FETCH_REQUEST, - ENTITIES_FETCH_SUCCESS, - ENTITIES_FETCH_FAIL, - ENTITIES_INVALIDATE_LIST, ENTITIES_TRANSACTION, importEntities, deleteEntities, - dismissEntities, - entitiesFetchRequest, - entitiesFetchSuccess, - entitiesFetchFail, - invalidateEntityList, entitiesTransaction, }; diff --git a/packages/pl-fe/src/entity-store/entities.ts b/packages/pl-fe/src/entity-store/entities.ts index 4f822f0ac..020d18471 100644 --- a/packages/pl-fe/src/entity-store/entities.ts +++ b/packages/pl-fe/src/entity-store/entities.ts @@ -1,17 +1,11 @@ -import type { Account, Group, GroupMember, GroupRelationship } from 'pl-api'; +import type { Account } from 'pl-api'; enum Entities { ACCOUNTS = 'Accounts', - GROUPS = 'Groups', - GROUP_MEMBERSHIPS = 'GroupMemberships', - GROUP_RELATIONSHIPS = 'GroupRelationships', } interface EntityTypes { [Entities.ACCOUNTS]: Account; - [Entities.GROUPS]: Group; - [Entities.GROUP_MEMBERSHIPS]: GroupMember; - [Entities.GROUP_RELATIONSHIPS]: GroupRelationship; } export { Entities, type EntityTypes }; diff --git a/packages/pl-fe/src/entity-store/hooks/types.ts b/packages/pl-fe/src/entity-store/hooks/types.ts index f838e8af9..ace66c4b1 100644 --- a/packages/pl-fe/src/entity-store/hooks/types.ts +++ b/packages/pl-fe/src/entity-store/hooks/types.ts @@ -4,24 +4,6 @@ import type { BaseSchema, BaseIssue } from 'valibot'; type EntitySchema = BaseSchema>; -/** - * Tells us where to find/store the entity in the cache. - * This value is accepted in hooks, but needs to be parsed into an `EntitiesPath` - * before being passed to the store. - */ -type ExpandedEntitiesPath = [ - /** Name of the entity type for use in the global cache, eg `'Notification'`. */ - entityType: Entities, - /** - * Name of a particular index of this entity type. - * Multiple params get combined into one string with a `:` separator. - */ - ...listKeys: string[], -]; - -/** Used to look up an entity in a list. */ -type EntitiesPath = [entityType: Entities, listKey: string]; - /** Used to look up a single entity by its ID. */ type EntityPath = [entityType: Entities, entityId: string]; @@ -37,11 +19,4 @@ interface EntityCallbacks { */ type EntityFn = (value: T) => Promise; -export type { - EntitySchema, - ExpandedEntitiesPath, - EntitiesPath, - EntityPath, - EntityCallbacks, - EntityFn, -}; +export type { EntitySchema, EntityPath, EntityCallbacks, EntityFn }; diff --git a/packages/pl-fe/src/entity-store/hooks/use-batched-entities.ts b/packages/pl-fe/src/entity-store/hooks/use-batched-entities.ts deleted file mode 100644 index b2e21507a..000000000 --- a/packages/pl-fe/src/entity-store/hooks/use-batched-entities.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect } from 'react'; -import * as v from 'valibot'; - -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { useGetState } from '@/hooks/use-get-state'; -import { filteredArray } from '@/schemas/utils'; - -import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions'; -import { selectCache, selectListState, useListState } from '../selectors'; - -import { parseEntitiesPath } from './utils'; - -import type { Entity } from '../types'; -import type { EntitiesPath, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types'; -import type { RootState } from '@/store'; - -interface UseBatchedEntitiesOpts { - schema?: EntitySchema; - enabled?: boolean; -} - -const useBatchedEntities = ( - expandedPath: ExpandedEntitiesPath, - ids: string[], - entityFn: EntityFn, - opts: UseBatchedEntitiesOpts = {}, -) => { - const getState = useGetState(); - const dispatch = useAppDispatch(); - const { entityType, listKey, path } = parseEntitiesPath(expandedPath); - const schema = opts.schema ?? v.custom(() => true); - - const isEnabled = opts.enabled ?? true; - const isFetching = useListState(path, 'fetching'); - const lastFetchedAt = useListState(path, 'lastFetchedAt'); - const isFetched = useListState(path, 'fetched'); - const isInvalid = useListState(path, 'invalid'); - const error = useListState(path, 'error'); - - /** Get IDs of entities not yet in the store. */ - const filteredIds = useAppSelector((state) => { - const cache = selectCache(state, path); - if (!cache) return ids; - return ids.filter((id) => !cache.store[id]); - }); - - const entityMap = useAppSelector((state) => selectEntityMap(state, path, ids)); - - const fetchEntities = async () => { - const isFetching = selectListState(getState(), path, 'fetching'); - if (isFetching) return; - - dispatch(entitiesFetchRequest(entityType, listKey)); - try { - const response = await entityFn(filteredIds); - const entities = v.parse(filteredArray(schema), response); - dispatch( - entitiesFetchSuccess(entities, entityType, listKey, 'end', { - next: null, - prev: null, - totalCount: undefined, - fetching: false, - fetched: true, - error: null, - lastFetchedAt: new Date(), - invalid: false, - }), - ); - } catch (e) { - dispatch(entitiesFetchFail(entityType, listKey, e)); - } - }; - - useEffect(() => { - if (filteredIds.length && isEnabled) { - fetchEntities(); - } - }, [filteredIds.length]); - - return { - entityMap, - isFetching, - lastFetchedAt, - isFetched, - isError: !!error, - isInvalid, - }; -}; - -const selectEntityMap = ( - state: RootState, - path: EntitiesPath, - entityIds: string[], -): Record => { - const cache = selectCache(state, path); - - return entityIds.reduce>((result, id) => { - const entity = cache?.store[id]; - if (entity) { - result[id] = entity as TEntity; - } - return result; - }, {}); -}; - -export { useBatchedEntities }; diff --git a/packages/pl-fe/src/entity-store/hooks/use-create-entity.ts b/packages/pl-fe/src/entity-store/hooks/use-create-entity.ts deleted file mode 100644 index 348726210..000000000 --- a/packages/pl-fe/src/entity-store/hooks/use-create-entity.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as v from 'valibot'; - -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useLoading } from '@/hooks/use-loading'; - -import { importEntities } from '../actions'; - -import { parseEntitiesPath } from './utils'; - -import type { Entity } from '../types'; -import type { EntityCallbacks, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types'; -import type { PlfeResponse } from '@/api'; - -interface UseCreateEntityOpts< - TEntity extends Entity = Entity, - TTransformedEntity extends Entity = TEntity, -> { - schema?: EntitySchema; - transform?: (schema: TEntity) => TTransformedEntity; -} - -const useCreateEntity = < - TEntity extends Entity = Entity, - TTransformedEntity extends Entity = TEntity, - Data = unknown, ->( - expandedPath: ExpandedEntitiesPath, - entityFn: EntityFn, - opts: UseCreateEntityOpts = {}, -) => { - const dispatch = useAppDispatch(); - - const [isSubmitting, setPromise] = useLoading(); - const { entityType, listKey } = parseEntitiesPath(expandedPath); - - const createEntity = async ( - data: Data, - callbacks: EntityCallbacks = {}, - ): Promise => { - const result = await setPromise(entityFn(data)); - const schema = opts.schema ?? v.custom(() => true); - let entity: TEntity | TTransformedEntity = v.parse(schema, result); - if (opts.transform) entity = opts.transform(entity); - - // TODO: optimistic updating - dispatch(importEntities([entity], entityType, listKey, 'start')); - - if (callbacks.onSuccess) { - callbacks.onSuccess(entity as TTransformedEntity); - } - }; - - return { - createEntity, - isSubmitting, - }; -}; - -export { useCreateEntity }; diff --git a/packages/pl-fe/src/entity-store/hooks/use-delete-entity.ts b/packages/pl-fe/src/entity-store/hooks/use-delete-entity.ts deleted file mode 100644 index 844c99358..000000000 --- a/packages/pl-fe/src/entity-store/hooks/use-delete-entity.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useGetState } from '@/hooks/use-get-state'; -import { useLoading } from '@/hooks/use-loading'; - -import { deleteEntities, importEntities } from '../actions'; - -import type { Entities } from '../entities'; -import type { EntityCallbacks, EntityFn } from './types'; - -/** - * Optimistically deletes an entity from the store. - * This hook should be used to globally delete an entity from all lists. - * To remove an entity from a single list, see `useDismissEntity`. - */ -const useDeleteEntity = (entityType: Entities, entityFn: EntityFn) => { - const dispatch = useAppDispatch(); - const getState = useGetState(); - const [isSubmitting, setPromise] = useLoading(); - - const deleteEntity = async ( - entityId: string, - callbacks: EntityCallbacks = {}, - ): Promise => { - // Get the entity before deleting, so we can reverse the action if the API request fails. - const entity = getState().entities[entityType]?.store[entityId]; - - // Optimistically delete the entity from the _store_ but keep the lists in tact. - dispatch(deleteEntities([entityId], entityType, { preserveLists: true })); - - try { - await setPromise(entityFn(entityId)); - - // Success - finish deleting entity from the state. - dispatch(deleteEntities([entityId], entityType)); - - if (callbacks.onSuccess) { - callbacks.onSuccess(entityId); - } - } catch (e) { - if (entity) { - // If the API failed, reimport the entity. - dispatch(importEntities([entity], entityType)); - } - - if (callbacks.onError) { - callbacks.onError(e); - } - } - }; - - return { - deleteEntity, - isSubmitting, - }; -}; - -export { useDeleteEntity }; diff --git a/packages/pl-fe/src/entity-store/hooks/use-dismiss-entity.ts b/packages/pl-fe/src/entity-store/hooks/use-dismiss-entity.ts deleted file mode 100644 index e542da873..000000000 --- a/packages/pl-fe/src/entity-store/hooks/use-dismiss-entity.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useLoading } from '@/hooks/use-loading'; - -import { dismissEntities } from '../actions'; - -import { parseEntitiesPath } from './utils'; - -import type { EntityFn, ExpandedEntitiesPath } from './types'; - -/** - * Removes an entity from a specific list. - * To remove an entity globally from all lists, see `useDeleteEntity`. - */ -const useDismissEntity = (expandedPath: ExpandedEntitiesPath, entityFn: EntityFn) => { - const dispatch = useAppDispatch(); - - const [isLoading, setPromise] = useLoading(); - const { entityType, listKey } = parseEntitiesPath(expandedPath); - - // TODO: optimistic dismissing - const dismissEntity = async (entityId: string) => { - const result = await setPromise(entityFn(entityId)); - dispatch(dismissEntities([entityId], entityType, listKey)); - return result; - }; - - return { - dismissEntity, - isLoading, - }; -}; - -export { useDismissEntity }; diff --git a/packages/pl-fe/src/entity-store/hooks/use-entities.ts b/packages/pl-fe/src/entity-store/hooks/use-entities.ts deleted file mode 100644 index 9441b2e78..000000000 --- a/packages/pl-fe/src/entity-store/hooks/use-entities.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { useEffect } from 'react'; -import * as v from 'valibot'; - -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { useGetState } from '@/hooks/use-get-state'; -import { filteredArray } from '@/schemas/utils'; - -import { - entitiesFetchFail, - entitiesFetchRequest, - entitiesFetchSuccess, - invalidateEntityList, -} from '../actions'; -import { selectEntities, selectListState, useListState } from '../selectors'; - -import { parseEntitiesPath } from './utils'; - -import type { Entity } from '../types'; -import type { EntityFn, EntitySchema, ExpandedEntitiesPath } from './types'; -import type { PaginatedResponse } from 'pl-api'; - -/** Additional options for the hook. */ -interface UseEntitiesOpts { - /** A valibot schema to parse the API entities. */ - schema?: EntitySchema; - /** - * Time (milliseconds) until this query becomes stale and should be refetched. - * It is 1 minute by default, and can be set to `Infinity` to opt-out of automatic fetching. - */ - staleTime?: number; - /** A flag to potentially disable sending requests to the API. */ - enabled?: boolean; - transform?: (schema: TEntity) => TTransformedEntity; -} - -/** A hook for fetching and displaying API entities. */ -const useEntities = ( - /** Tells us where to find/store the entity in the cache. */ - expandedPath: ExpandedEntitiesPath, - /** API route to GET, eg `'/api/v1/notifications'`. If undefined, nothing will be fetched. */ - entityFn: EntityFn, - /** Additional options for the hook. */ - opts: UseEntitiesOpts = {}, -) => { - const dispatch = useAppDispatch(); - const getState = useGetState(); - - const { entityType, listKey, path } = parseEntitiesPath(expandedPath); - const entities = useAppSelector((state) => selectEntities(state, path)); - const schema = opts.schema ?? v.custom(() => true); - - const isEnabled = opts.enabled ?? true; - const isFetching = useListState(path, 'fetching'); - const lastFetchedAt = useListState(path, 'lastFetchedAt'); - const isFetched = useListState(path, 'fetched'); - const isError = !!useListState(path, 'error'); - const totalCount = useListState(path, 'totalCount'); - const isInvalid = useListState(path, 'invalid'); - - const next = useListState(path, 'next'); - const prev = useListState(path, 'prev'); - - const fetchPage = async ( - req: () => Promise>, - pos: 'start' | 'end', - overwrite = false, - ): Promise => { - // Get `isFetching` state from the store again to prevent race conditions. - const isFetching = selectListState(getState(), path, 'fetching'); - if (isFetching) return; - - dispatch(entitiesFetchRequest(entityType, listKey)); - try { - const response = await req(); - const entities = v.parse(filteredArray(schema), response); - const transformedEntities = opts.transform && entities.map(opts.transform); - - dispatch( - entitiesFetchSuccess( - transformedEntities ?? entities, - entityType, - listKey, - pos, - { - next: response.next, - prev: response.previous, - totalCount: undefined, - fetching: false, - fetched: true, - error: null, - lastFetchedAt: new Date(), - invalid: false, - }, - overwrite, - ), - ); - } catch (error) { - dispatch(entitiesFetchFail(entityType, listKey, error)); - } - }; - - const fetchEntities = async (): Promise => { - await fetchPage(entityFn, 'end', true); - }; - - const fetchNextPage = async (): Promise => { - if (next) { - await fetchPage(() => next(), 'end'); - } - }; - - const fetchPreviousPage = async (): Promise => { - if (prev) { - await fetchPage(() => prev(), 'start'); - } - }; - - const invalidate = () => { - dispatch(invalidateEntityList(entityType, listKey)); - }; - - const staleTime = opts.staleTime ?? 60000; - - useEffect(() => { - if (!isEnabled) return; - if (isFetching) return; - const isUnset = !lastFetchedAt; - const isStale = lastFetchedAt ? Date.now() >= lastFetchedAt.getTime() + staleTime : false; - - if (isInvalid || isUnset || isStale) { - fetchEntities(); - } - }, [isEnabled, ...path]); - - return { - entities, - fetchEntities, - fetchNextPage, - fetchPreviousPage, - hasNextPage: !!next, - hasPreviousPage: !!prev, - totalCount, - isError, - isFetched, - isFetching, - isLoading: isFetching && entities.length === 0, - invalidate, - /** The `X-Total-Count` from the API if available, or the length of items in the store. */ - count: typeof totalCount === 'number' ? totalCount : entities.length, - }; -}; - -export { useEntities }; diff --git a/packages/pl-fe/src/entity-store/hooks/utils.ts b/packages/pl-fe/src/entity-store/hooks/utils.ts deleted file mode 100644 index 917a72e67..000000000 --- a/packages/pl-fe/src/entity-store/hooks/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { EntitiesPath, ExpandedEntitiesPath } from './types'; - -const parseEntitiesPath = (expandedPath: ExpandedEntitiesPath) => { - const [entityType, ...listKeys] = expandedPath; - const listKey = (listKeys || []).join(':'); - const path: EntitiesPath = [entityType, listKey]; - - return { - entityType, - listKey, - path, - }; -}; - -export { parseEntitiesPath }; diff --git a/packages/pl-fe/src/entity-store/reducer.ts b/packages/pl-fe/src/entity-store/reducer.ts index 34c6a172e..599af63d7 100644 --- a/packages/pl-fe/src/entity-store/reducer.ts +++ b/packages/pl-fe/src/entity-store/reducer.ts @@ -3,11 +3,6 @@ import { create, type Immutable, type Draft } from 'mutative'; import { ENTITIES_IMPORT, ENTITIES_DELETE, - ENTITIES_DISMISS, - ENTITIES_FETCH_REQUEST, - ENTITIES_FETCH_SUCCESS, - ENTITIES_FETCH_FAIL, - ENTITIES_INVALIDATE_LIST, ENTITIES_TRANSACTION, type EntityAction, type DeleteEntitiesOpts, @@ -58,18 +53,6 @@ const importEntities = ( } draft[entityType] = cache; - - if (entityType === Entities.GROUPS) { - importEntities( - draft, - Entities.GROUP_RELATIONSHIPS, - entities - .map((entity: any) => entity?.relationship) - .filter((relationship: any) => relationship), - listKey, - pos, - ); - } }; const deleteEntities = ( @@ -99,53 +82,6 @@ const deleteEntities = ( draft[entityType] = cache; }; -const dismissEntities = ( - draft: Draft, - entityType: string, - ids: Iterable, - listKey: string, -) => { - const cache = draft[entityType] ?? createCache(); - const list = cache.lists[listKey]; - - if (list) { - for (const id of ids) { - list.ids.delete(id); - - if (typeof list.state.totalCount === 'number') { - list.state.totalCount--; - } - } - - draft[entityType] = cache; - } -}; - -const setFetching = ( - draft: Draft, - entityType: string, - listKey: string | undefined, - isFetching: boolean, - error?: any, -) => { - const cache = draft[entityType] ?? createCache(); - - if (typeof listKey === 'string') { - const list = cache.lists[listKey] ?? createList(); - list.state.fetching = isFetching; - list.state.error = error; - cache.lists[listKey] = list; - } - - draft[entityType] = cache; -}; - -const invalidateEntityList = (draft: Draft, entityType: string, listKey: string) => { - const cache = draft[entityType] ?? createCache(); - const list = cache.lists[listKey] ?? createList(); - list.state.invalid = true; -}; - const doTransaction = (draft: Draft, transaction: EntitiesTransaction) => { for (const [entityType, changes] of Object.entries(transaction)) { const cache = draft[entityType] ?? createCache(); @@ -173,38 +109,6 @@ const reducer = (state: Readonly = {}, action: EntityAction): State => { return create(state, (draft) => { deleteEntities(draft, action.entityType, action.ids, action.opts); }); - case ENTITIES_DISMISS: - return create(state, (draft) => { - dismissEntities(draft, action.entityType, action.ids, action.listKey); - }); - case ENTITIES_FETCH_SUCCESS: - return create( - state, - (draft) => { - importEntities( - draft, - action.entityType, - action.entities, - action.listKey, - action.pos, - action.newState, - action.overwrite, - ); - }, - { enableAutoFreeze: true }, - ); - case ENTITIES_FETCH_REQUEST: - return create(state, (draft) => { - setFetching(draft, action.entityType, action.listKey, true); - }); - case ENTITIES_FETCH_FAIL: - return create(state, (draft) => { - setFetching(draft, action.entityType, action.listKey, false, action.error); - }); - case ENTITIES_INVALIDATE_LIST: - return create(state, (draft) => { - invalidateEntityList(draft, action.entityType, action.listKey); - }); case ENTITIES_TRANSACTION: return create(state, (draft) => { doTransaction(draft, action.transaction); diff --git a/packages/pl-fe/src/entity-store/selectors.ts b/packages/pl-fe/src/entity-store/selectors.ts index 9927b5559..ce4019e98 100644 --- a/packages/pl-fe/src/entity-store/selectors.ts +++ b/packages/pl-fe/src/entity-store/selectors.ts @@ -1,34 +1,6 @@ -import { useAppSelector } from '@/hooks/use-app-selector'; - -import type { EntitiesPath } from './hooks/types'; -import type { Entity, EntityListState } from './types'; +import type { Entity } from './types'; import type { RootState } from '@/store'; -/** Get cache at path from Redux. */ -const selectCache = (state: RootState, path: EntitiesPath) => state.entities[path[0]]; - -/** Get list at path from Redux. */ -const selectList = (state: RootState, path: EntitiesPath) => { - const [, ...listKeys] = path; - const listKey = listKeys.join(':'); - - return selectCache(state, path)?.lists[listKey]; -}; - -/** Select a particular item from a list state. */ -const selectListState = ( - state: RootState, - path: EntitiesPath, - key: K, -) => { - const listState = selectList(state, path)?.state; - return listState ? listState[key] : undefined; -}; - -/** Hook to get a particular item from a list state. */ -const useListState = (path: EntitiesPath, key: K) => - useAppSelector((state) => selectListState(state, path, key)); - /** Get a single entity by its ID from the store. */ const selectEntity = ( state: RootState, @@ -36,27 +8,6 @@ const selectEntity = ( id: string, ): TEntity | undefined => state.entities[entityType]?.store[id] as TEntity | undefined; -/** Get list of entities from Redux. */ -const selectEntities = ( - state: RootState, - path: EntitiesPath, -): readonly TEntity[] => { - const cache = selectCache(state, path); - const list = selectList(state, path); - - const entityIds = list?.ids; - - return entityIds - ? Array.from(entityIds).reduce((result, id) => { - const entity = cache?.store[id]; - if (entity) { - result.push(entity as TEntity); - } - return result; - }, []) - : []; -}; - /** Find an entity using a finder function. */ const findEntity = ( state: RootState, @@ -70,4 +21,4 @@ const findEntity = ( } }; -export { selectCache, selectListState, useListState, selectEntities, selectEntity, findEntity }; +export { selectEntity, findEntity }; diff --git a/packages/pl-fe/src/features/group/components/group-member-list-item.tsx b/packages/pl-fe/src/features/group/components/group-member-list-item.tsx index 43abae6ff..5eb2739ce 100644 --- a/packages/pl-fe/src/features/group/components/group-member-list-item.tsx +++ b/packages/pl-fe/src/features/group/components/group-member-list-item.tsx @@ -7,10 +7,7 @@ import { useAccount } from '@/api/hooks/accounts/use-account'; import Account from '@/components/account'; import DropdownMenu from '@/components/dropdown-menu/dropdown-menu'; import HStack from '@/components/ui/hstack'; -import { deleteEntities } from '@/entity-store/actions'; -import { Entities } from '@/entity-store/entities'; import PlaceholderAccount from '@/features/placeholder/components/placeholder-account'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useBlockGroupUserMutation } from '@/queries/groups/use-group-blocks'; import { useDemoteGroupMemberMutation, @@ -73,7 +70,6 @@ interface IGroupMemberListItem { } const GroupMemberListItem = ({ member, group }: IGroupMemberListItem) => { - const dispatch = useAppDispatch(); const intl = useIntl(); const { openModal } = useModalsActions(); @@ -117,7 +113,6 @@ const GroupMemberListItem = ({ member, group }: IGroupMemberListItem) => { onConfirm: () => { blockGroupMember(undefined, { onSuccess() { - dispatch(deleteEntities([member.id], Entities.GROUP_MEMBERSHIPS)); toast.success(intl.formatMessage(messages.blocked, { name: account?.acct })); }, }); diff --git a/packages/pl-fe/src/features/status/components/detailed-status.tsx b/packages/pl-fe/src/features/status/components/detailed-status.tsx index 56e2f32d1..66c74ded6 100644 --- a/packages/pl-fe/src/features/status/components/detailed-status.tsx +++ b/packages/pl-fe/src/features/status/components/detailed-status.tsx @@ -14,6 +14,7 @@ import Icon from '@/components/ui/icon'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import Emojify from '@/features/emoji/emojify'; +import { useGroupQuery } from '@/queries/groups/use-group'; import StatusInteractionBar from './status-interaction-bar'; import StatusTypeIcon from './status-type-icon'; @@ -39,12 +40,14 @@ const DetailedStatus: React.FC = ({ const node = useRef(null); + const { data: group } = useGroupQuery(status.group_id ?? undefined); + const handleOpenCompareHistoryModal = () => { onOpenCompareHistoryModal(status); }; const renderStatusInfo = () => { - if (status.group) { + if (status.group_id) { return (
= ({ /> } text={ - - - - - - - - ), - }} - /> + group ? ( + + + + + + + + ), + }} + /> + ) : ( + + ) } />
diff --git a/packages/pl-fe/src/modals/manage-group-modal/index.tsx b/packages/pl-fe/src/modals/manage-group-modal/index.tsx index 03e718b24..afe246e21 100644 --- a/packages/pl-fe/src/modals/manage-group-modal/index.tsx +++ b/packages/pl-fe/src/modals/manage-group-modal/index.tsx @@ -2,9 +2,9 @@ import React, { useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import * as v from 'valibot'; -import { useCreateGroup } from '@/api/hooks/groups/use-create-group'; import Modal from '@/components/ui/modal'; import Stack from '@/components/ui/stack'; +import { useCreateGroupMutation } from '@/queries/groups/use-group'; import toast from '@/toast'; import ConfirmationStep from './steps/confirmation-step'; @@ -32,7 +32,7 @@ const CreateGroupModal: React.FC = ({ onClose }) => { }); const [currentStep, setCurrentStep] = useState(Steps.ONE); - const { createGroup, isSubmitting } = useCreateGroup(); + const { mutate: createGroup, isPending } = useCreateGroupMutation(); const handleClose = () => { onClose('CREATE_GROUP'); @@ -55,8 +55,11 @@ const CreateGroupModal: React.FC = ({ onClose }) => { setCurrentStep(Steps.TWO); setGroup(group); }, - onError(error: { response?: PlfeResponse }) { - const msg = v.safeParse(v.object({ error: v.string() }), error?.response?.json); + onError(error) { + const msg = v.safeParse( + v.object({ error: v.string() }), + (error as { response?: PlfeResponse })?.response?.json, + ); if (msg.success) { toast.error(msg.output.error); } @@ -89,7 +92,7 @@ const CreateGroupModal: React.FC = ({ onClose }) => { title={renderModalTitle()} confirmationAction={handleNextStep} confirmationText={confirmationText} - confirmationDisabled={isSubmitting} + confirmationDisabled={isPending} onClose={handleClose} > {renderStep()} diff --git a/packages/pl-fe/src/normalizers/status.ts b/packages/pl-fe/src/normalizers/status.ts index 4c5beb158..71caace8a 100644 --- a/packages/pl-fe/src/normalizers/status.ts +++ b/packages/pl-fe/src/normalizers/status.ts @@ -121,9 +121,6 @@ const normalizeStatus = ( }; } - // Normalize group - const group = status.group ?? null; - return { account_id: status.account.id, reblog_id: status.reblog?.id ?? null, @@ -136,7 +133,6 @@ const normalizeStatus = ( quote_id: status.quote_id ?? null, mentions, event, - group, media_attachments, search_index: searchIndex, }; diff --git a/packages/pl-fe/src/pages/groups/edit-group.tsx b/packages/pl-fe/src/pages/groups/edit-group.tsx index fc5043371..88d2b8670 100644 --- a/packages/pl-fe/src/pages/groups/edit-group.tsx +++ b/packages/pl-fe/src/pages/groups/edit-group.tsx @@ -1,7 +1,6 @@ -import React, { useState } from 'react'; +import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useUpdateGroup } from '@/api/hooks/groups/use-update-group'; import Button from '@/components/ui/button'; import Column from '@/components/ui/column'; import Form from '@/components/ui/form'; @@ -18,9 +17,12 @@ import { useImageField } from '@/hooks/forms/use-image-field'; import { useTextField } from '@/hooks/forms/use-text-field'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useInstance } from '@/hooks/use-instance'; -import { useGroupQuery } from '@/queries/groups/use-group'; +import { useGroupQuery, useUpdateGroupMutation } from '@/queries/groups/use-group'; import toast from '@/toast'; import { unescapeHTML } from '@/utils/html'; + +import type { PlfeResponse } from '@/api'; + const messages = defineMessages({ heading: { id: 'navigation_bar.edit_group', defaultMessage: 'Edit Group' }, groupNamePlaceholder: { @@ -41,9 +43,7 @@ const EditGroup: React.FC = () => { const instance = useInstance(); const { data: group, isLoading } = useGroupQuery(groupId); - const { updateGroup } = useUpdateGroup(groupId); - - const [isSubmitting, setIsSubmitting] = useState(false); + const { mutate: updateGroup, isPending: isUpdatePending } = useUpdateGroupMutation(groupId); const avatar = useImageField({ maxPixels: 400 * 400, @@ -67,8 +67,6 @@ const EditGroup: React.FC = () => { .join(','); const handleSubmit = async () => { - setIsSubmitting(true); - await updateGroup( { display_name: displayName.value, @@ -81,16 +79,15 @@ const EditGroup: React.FC = () => { toast.success(intl.formatMessage(messages.groupSaved)); }, onError(error) { - const message = error.response?.json?.error; + const response = (error as { response?: PlfeResponse })?.response; + const message = response?.json?.error; - if (error.response?.status === 422 && typeof message !== 'undefined') { + if (response?.status === 422 && typeof message !== 'undefined') { toast.error(message); } }, }, ); - - setIsSubmitting(false); }; if (isLoading) { @@ -101,8 +98,8 @@ const EditGroup: React.FC = () => {
- - + +
{ - diff --git a/packages/pl-fe/src/pages/groups/manage-group.tsx b/packages/pl-fe/src/pages/groups/manage-group.tsx index 76611b632..c1ea5ffe5 100644 --- a/packages/pl-fe/src/pages/groups/manage-group.tsx +++ b/packages/pl-fe/src/pages/groups/manage-group.tsx @@ -3,7 +3,6 @@ import { GroupRoles } from 'pl-api'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useDeleteGroup } from '@/api/hooks/groups/use-delete-group'; import List, { ListItem } from '@/components/list'; import { CardBody, CardHeader, CardTitle } from '@/components/ui/card'; import Column from '@/components/ui/column'; @@ -12,7 +11,7 @@ import Text from '@/components/ui/text'; import Emojify from '@/features/emoji/emojify'; import ColumnForbidden from '@/features/ui/components/column-forbidden'; import { manageGroupRoute } from '@/features/ui/router'; -import { useGroupQuery } from '@/queries/groups/use-group'; +import { useDeleteGroupMutation, useGroupQuery } from '@/queries/groups/use-group'; import { useModalsActions } from '@/stores/modals'; import toast from '@/toast'; @@ -43,7 +42,7 @@ const ManageGroup: React.FC = () => { const { data: group } = useGroupQuery(groupId, true); - const deleteGroup = useDeleteGroup(); + const { mutate: deleteGroup } = useDeleteGroupMutation(groupId); const isOwner = group?.relationship?.role === GroupRoles.OWNER; @@ -68,7 +67,7 @@ const ManageGroup: React.FC = () => { message: intl.formatMessage(messages.deleteMessage), confirm: intl.formatMessage(messages.deleteConfirm), onConfirm: () => { - deleteGroup.mutate(groupId, { + deleteGroup(undefined, { onSuccess() { toast.success(intl.formatMessage(messages.deleteSuccess)); navigate({ to: '/groups' }); diff --git a/packages/pl-fe/src/queries/groups/use-group.ts b/packages/pl-fe/src/queries/groups/use-group.ts index 00931c5d3..e05330e88 100644 --- a/packages/pl-fe/src/queries/groups/use-group.ts +++ b/packages/pl-fe/src/queries/groups/use-group.ts @@ -1,10 +1,12 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo } from 'react'; import { useClient } from '@/hooks/use-client'; import { useGroupRelationshipQuery } from './use-group-relationship'; +import type { CreateGroupParams, UpdateGroupParams } from 'pl-api'; + const useGroupQuery = (groupId?: string, withRelationship = true) => { const client = useClient(); @@ -30,4 +32,46 @@ const useGroupQuery = (groupId?: string, withRelationship = true) => { ); }; -export { useGroupQuery }; +const useCreateGroupMutation = () => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['groups', 'create'], + mutationFn: (params: CreateGroupParams) => client.experimental.groups.createGroup(params), + onSuccess: (data) => { + queryClient.setQueryData(['groups', data.id], data); + queryClient.invalidateQueries({ queryKey: ['groupLists', 'myGroups'] }); + }, + }); +}; + +const useUpdateGroupMutation = (groupId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['groups', 'update'], + mutationFn: (params: UpdateGroupParams) => + client.experimental.groups.updateGroup(groupId, params), + onSuccess: (data) => { + queryClient.setQueryData(['groups', data.id], data); + }, + }); +}; + +const useDeleteGroupMutation = (groupId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['groups', 'delete'], + mutationFn: () => client.experimental.groups.deleteGroup(groupId), + onSuccess: () => { + queryClient.removeQueries({ queryKey: ['groups', groupId] }); + queryClient.invalidateQueries({ queryKey: ['groupLists', 'myGroups'] }); + }, + }); +}; + +export { useGroupQuery, useCreateGroupMutation, useUpdateGroupMutation, useDeleteGroupMutation };