nicolium: group migrations
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -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<BaseGroup>(['groups', group.id], group);
|
||||
if (group.relationship) {
|
||||
queryClient.setQueryData<BaseGroup>(['groupRelationships', group.id], group.relationship);
|
||||
}
|
||||
}
|
||||
if (!isEmpty(polls)) {
|
||||
for (const poll of Object.values(polls)) {
|
||||
queryClient.setQueryData<BasePoll>(['statuses', 'polls', poll.id], poll);
|
||||
|
||||
@ -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<Group, Group, CreateGroupParams>(
|
||||
[Entities.GROUPS, 'search', ''],
|
||||
(params: CreateGroupParams) => client.experimental.groups.createGroup(params),
|
||||
);
|
||||
|
||||
return {
|
||||
createGroup: createEntity,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
export { useCreateGroup };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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<IReplyButton> = ({
|
||||
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<IReplyButton> = ({
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return status.group ? (
|
||||
<GroupPopover group={status.group} isEnabled={replyDisabled}>
|
||||
return group ? (
|
||||
<GroupPopover group={group} isEnabled={replyDisabled}>
|
||||
{replyButton}
|
||||
</GroupPopover>
|
||||
) : (
|
||||
@ -741,8 +740,8 @@ const MenuButton: React.FC<IMenuButton> = ({
|
||||
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<IMenuButton> = ({
|
||||
});
|
||||
}
|
||||
|
||||
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<IMenuButton> = ({
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -205,13 +205,14 @@ const Status: React.FC<IStatus> = (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]
|
||||
|
||||
@ -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<string>, 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<typeof importEntities>
|
||||
| ReturnType<typeof deleteEntities>
|
||||
| ReturnType<typeof dismissEntities>
|
||||
| ReturnType<typeof entitiesFetchRequest>
|
||||
| ReturnType<typeof entitiesFetchSuccess>
|
||||
| ReturnType<typeof entitiesFetchFail>
|
||||
| ReturnType<typeof invalidateEntityList>
|
||||
| ReturnType<typeof entitiesTransaction>;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -4,24 +4,6 @@ import type { BaseSchema, BaseIssue } from 'valibot';
|
||||
|
||||
type EntitySchema<TEntity extends Entity = Entity> = BaseSchema<any, TEntity, BaseIssue<unknown>>;
|
||||
|
||||
/**
|
||||
* 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<Value, Error = unknown> {
|
||||
*/
|
||||
type EntityFn<T> = (value: T) => Promise<any>;
|
||||
|
||||
export type {
|
||||
EntitySchema,
|
||||
ExpandedEntitiesPath,
|
||||
EntitiesPath,
|
||||
EntityPath,
|
||||
EntityCallbacks,
|
||||
EntityFn,
|
||||
};
|
||||
export type { EntitySchema, EntityPath, EntityCallbacks, EntityFn };
|
||||
|
||||
@ -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<TEntity extends Entity> {
|
||||
schema?: EntitySchema<TEntity>;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const useBatchedEntities = <TEntity extends Entity>(
|
||||
expandedPath: ExpandedEntitiesPath,
|
||||
ids: string[],
|
||||
entityFn: EntityFn<string[]>,
|
||||
opts: UseBatchedEntitiesOpts<TEntity> = {},
|
||||
) => {
|
||||
const getState = useGetState();
|
||||
const dispatch = useAppDispatch();
|
||||
const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
|
||||
const schema = opts.schema ?? v.custom<TEntity>(() => 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<TEntity>(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 = <TEntity extends Entity>(
|
||||
state: RootState,
|
||||
path: EntitiesPath,
|
||||
entityIds: string[],
|
||||
): Record<string, TEntity> => {
|
||||
const cache = selectCache(state, path);
|
||||
|
||||
return entityIds.reduce<Record<string, TEntity>>((result, id) => {
|
||||
const entity = cache?.store[id];
|
||||
if (entity) {
|
||||
result[id] = entity as TEntity;
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export { useBatchedEntities };
|
||||
@ -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<TEntity>;
|
||||
transform?: (schema: TEntity) => TTransformedEntity;
|
||||
}
|
||||
|
||||
const useCreateEntity = <
|
||||
TEntity extends Entity = Entity,
|
||||
TTransformedEntity extends Entity = TEntity,
|
||||
Data = unknown,
|
||||
>(
|
||||
expandedPath: ExpandedEntitiesPath,
|
||||
entityFn: EntityFn<Data>,
|
||||
opts: UseCreateEntityOpts<TEntity, TTransformedEntity> = {},
|
||||
) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isSubmitting, setPromise] = useLoading();
|
||||
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
||||
|
||||
const createEntity = async (
|
||||
data: Data,
|
||||
callbacks: EntityCallbacks<TTransformedEntity, { response?: PlfeResponse }> = {},
|
||||
): Promise<void> => {
|
||||
const result = await setPromise(entityFn(data));
|
||||
const schema = opts.schema ?? v.custom<TEntity>(() => 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 };
|
||||
@ -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<string>) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const getState = useGetState();
|
||||
const [isSubmitting, setPromise] = useLoading();
|
||||
|
||||
const deleteEntity = async (
|
||||
entityId: string,
|
||||
callbacks: EntityCallbacks<string> = {},
|
||||
): Promise<void> => {
|
||||
// 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 };
|
||||
@ -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<string>) => {
|
||||
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 };
|
||||
@ -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<TEntity extends Entity, TTransformedEntity extends Entity> {
|
||||
/** A valibot schema to parse the API entities. */
|
||||
schema?: EntitySchema<TEntity>;
|
||||
/**
|
||||
* 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 = <TEntity extends Entity, TTransformedEntity extends Entity = TEntity>(
|
||||
/** 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<void>,
|
||||
/** Additional options for the hook. */
|
||||
opts: UseEntitiesOpts<TEntity, TTransformedEntity> = {},
|
||||
) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const getState = useGetState();
|
||||
|
||||
const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
|
||||
const entities = useAppSelector((state) => selectEntities<TTransformedEntity>(state, path));
|
||||
const schema = opts.schema ?? v.custom<TEntity>(() => 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<PaginatedResponse<any>>,
|
||||
pos: 'start' | 'end',
|
||||
overwrite = false,
|
||||
): Promise<void> => {
|
||||
// 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<void> => {
|
||||
await fetchPage(entityFn, 'end', true);
|
||||
};
|
||||
|
||||
const fetchNextPage = async (): Promise<void> => {
|
||||
if (next) {
|
||||
await fetchPage(() => next(), 'end');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPreviousPage = async (): Promise<void> => {
|
||||
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 };
|
||||
@ -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 };
|
||||
@ -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<State>,
|
||||
entityType: string,
|
||||
ids: Iterable<string>,
|
||||
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<State>,
|
||||
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<State>, entityType: string, listKey: string) => {
|
||||
const cache = draft[entityType] ?? createCache();
|
||||
const list = cache.lists[listKey] ?? createList();
|
||||
list.state.invalid = true;
|
||||
};
|
||||
|
||||
const doTransaction = (draft: Draft<State>, transaction: EntitiesTransaction) => {
|
||||
for (const [entityType, changes] of Object.entries(transaction)) {
|
||||
const cache = draft[entityType] ?? createCache();
|
||||
@ -173,38 +109,6 @@ const reducer = (state: Readonly<State> = {}, 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);
|
||||
|
||||
@ -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 = <K extends keyof EntityListState>(
|
||||
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 = <K extends keyof EntityListState>(path: EntitiesPath, key: K) =>
|
||||
useAppSelector((state) => selectListState(state, path, key));
|
||||
|
||||
/** Get a single entity by its ID from the store. */
|
||||
const selectEntity = <TEntity extends Entity>(
|
||||
state: RootState,
|
||||
@ -36,27 +8,6 @@ const selectEntity = <TEntity extends Entity>(
|
||||
id: string,
|
||||
): TEntity | undefined => state.entities[entityType]?.store[id] as TEntity | undefined;
|
||||
|
||||
/** Get list of entities from Redux. */
|
||||
const selectEntities = <TEntity extends Entity>(
|
||||
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<TEntity[]>((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 = <TEntity extends Entity>(
|
||||
state: RootState,
|
||||
@ -70,4 +21,4 @@ const findEntity = <TEntity extends Entity>(
|
||||
}
|
||||
};
|
||||
|
||||
export { selectCache, selectListState, useListState, selectEntities, selectEntity, findEntity };
|
||||
export { selectEntity, findEntity };
|
||||
|
||||
@ -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 }));
|
||||
},
|
||||
});
|
||||
|
||||
@ -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<IDetailedStatus> = ({
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: group } = useGroupQuery(status.group_id ?? undefined);
|
||||
|
||||
const handleOpenCompareHistoryModal = () => {
|
||||
onOpenCompareHistoryModal(status);
|
||||
};
|
||||
|
||||
const renderStatusInfo = () => {
|
||||
if (status.group) {
|
||||
if (status.group_id) {
|
||||
return (
|
||||
<div className='mb-4'>
|
||||
<StatusInfo
|
||||
@ -58,28 +61,32 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||
/>
|
||||
}
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='status.group'
|
||||
defaultMessage='Posted in {group}'
|
||||
values={{
|
||||
group: (
|
||||
<Link
|
||||
to='/groups/$groupId'
|
||||
params={{ groupId: status.group.id }}
|
||||
className='hover:underline'
|
||||
>
|
||||
<bdi className='truncate'>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<Emojify
|
||||
text={status.account.display_name}
|
||||
emojis={status.account.emojis}
|
||||
/>
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
group ? (
|
||||
<FormattedMessage
|
||||
id='status.group'
|
||||
defaultMessage='Posted in {group}'
|
||||
values={{
|
||||
group: (
|
||||
<Link
|
||||
to='/groups/$groupId'
|
||||
params={{ groupId: status.group.id }}
|
||||
className='hover:underline'
|
||||
>
|
||||
<bdi className='truncate'>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<Emojify
|
||||
text={status.account.display_name}
|
||||
emojis={status.account.emojis}
|
||||
/>
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage id='status.group.unknown' defaultMessage='Posted in a group' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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<BaseModalProps> = ({ onClose }) => {
|
||||
});
|
||||
const [currentStep, setCurrentStep] = useState<Steps>(Steps.ONE);
|
||||
|
||||
const { createGroup, isSubmitting } = useCreateGroup();
|
||||
const { mutate: createGroup, isPending } = useCreateGroupMutation();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose('CREATE_GROUP');
|
||||
@ -55,8 +55,11 @@ const CreateGroupModal: React.FC<BaseModalProps> = ({ 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<BaseModalProps> = ({ onClose }) => {
|
||||
title={renderModalTitle()}
|
||||
confirmationAction={handleNextStep}
|
||||
confirmationText={confirmationText}
|
||||
confirmationDisabled={isSubmitting}
|
||||
confirmationDisabled={isPending}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Stack space={2}>{renderStep()}</Stack>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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 = () => {
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className='relative mb-12 flex'>
|
||||
<HeaderPicker accept={attachmentTypes} disabled={isSubmitting} {...header} />
|
||||
<AvatarPicker accept={attachmentTypes} disabled={isSubmitting} {...avatar} />
|
||||
<HeaderPicker accept={attachmentTypes} disabled={isUpdatePending} {...header} />
|
||||
<AvatarPicker accept={attachmentTypes} disabled={isUpdatePending} {...avatar} />
|
||||
</div>
|
||||
<FormGroup
|
||||
labelText={
|
||||
@ -149,7 +146,7 @@ const EditGroup: React.FC = () => {
|
||||
</FormGroup>
|
||||
|
||||
<FormActions>
|
||||
<Button theme='primary' type='submit' disabled={isSubmitting} block>
|
||||
<Button theme='primary' type='submit' disabled={isUpdatePending} block>
|
||||
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
|
||||
@ -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' });
|
||||
|
||||
@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user