diff --git a/app/soapbox/entity-store/__tests__/reducer.test.ts b/app/soapbox/entity-store/__tests__/reducer.test.ts index f43150b68..1cfc19697 100644 --- a/app/soapbox/entity-store/__tests__/reducer.test.ts +++ b/app/soapbox/entity-store/__tests__/reducer.test.ts @@ -5,6 +5,7 @@ import { entitiesFetchRequest, entitiesFetchSuccess, importEntities, + incrementEntities, } from '../actions'; import reducer, { State } from '../reducer'; import { createListState } from '../utils'; @@ -167,4 +168,42 @@ test('dismiss items', () => { expect(result.TestEntity!.store).toMatchObject(state.TestEntity!.store); expect([...result.TestEntity!.lists.yolo!.ids]).toEqual(['2']); expect(result.TestEntity!.lists.yolo!.state.totalCount).toBe(1); +}); + +test('increment items', () => { + const state: State = { + TestEntity: { + store: { '1': { id: '1' }, '2': { id: '2' }, '3': { id: '3' } }, + lists: { + thingies: { + ids: new Set(['1', '2', '3']), + state: { ...createListState(), totalCount: 3 }, + }, + }, + }, + }; + + const action = incrementEntities('TestEntity', 'thingies', 1); + const result = reducer(state, action); + + expect(result.TestEntity!.lists.thingies!.state.totalCount).toBe(4); +}); + +test('decrement items', () => { + const state: State = { + TestEntity: { + store: { '1': { id: '1' }, '2': { id: '2' }, '3': { id: '3' } }, + lists: { + thingies: { + ids: new Set(['1', '2', '3']), + state: { ...createListState(), totalCount: 3 }, + }, + }, + }, + }; + + const action = incrementEntities('TestEntity', 'thingies', -1); + const result = reducer(state, action); + + expect(result.TestEntity!.lists.thingies!.state.totalCount).toBe(2); }); \ No newline at end of file diff --git a/app/soapbox/entity-store/actions.ts b/app/soapbox/entity-store/actions.ts index 8f4783c94..c3ba25559 100644 --- a/app/soapbox/entity-store/actions.ts +++ b/app/soapbox/entity-store/actions.ts @@ -3,6 +3,7 @@ import type { Entity, EntityListState } 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_INCREMENT = 'ENTITIES_INCREMENT' 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; @@ -40,6 +41,15 @@ function dismissEntities(ids: Iterable, entityType: string, listKey: str }; } +function incrementEntities(entityType: string, listKey: string, diff: number) { + return { + type: ENTITIES_INCREMENT, + entityType, + listKey, + diff, + }; +} + function entitiesFetchRequest(entityType: string, listKey?: string) { return { type: ENTITIES_FETCH_REQUEST, @@ -87,6 +97,7 @@ type EntityAction = ReturnType | ReturnType | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType @@ -96,6 +107,7 @@ export { ENTITIES_IMPORT, ENTITIES_DELETE, ENTITIES_DISMISS, + ENTITIES_INCREMENT, ENTITIES_FETCH_REQUEST, ENTITIES_FETCH_SUCCESS, ENTITIES_FETCH_FAIL, @@ -103,6 +115,7 @@ export { importEntities, deleteEntities, dismissEntities, + incrementEntities, entitiesFetchRequest, entitiesFetchSuccess, entitiesFetchFail, diff --git a/app/soapbox/entity-store/hooks/index.ts b/app/soapbox/entity-store/hooks/index.ts index 09fe0c960..d113c505a 100644 --- a/app/soapbox/entity-store/hooks/index.ts +++ b/app/soapbox/entity-store/hooks/index.ts @@ -3,4 +3,5 @@ export { useEntity } from './useEntity'; export { useEntityActions } from './useEntityActions'; export { useCreateEntity } from './useCreateEntity'; export { useDeleteEntity } from './useDeleteEntity'; -export { useDismissEntity } from './useDismissEntity'; \ No newline at end of file +export { useDismissEntity } from './useDismissEntity'; +export { useIncrementEntity } from './useIncrementEntity'; \ No newline at end of file diff --git a/app/soapbox/entity-store/hooks/useIncrementEntity.ts b/app/soapbox/entity-store/hooks/useIncrementEntity.ts new file mode 100644 index 000000000..5f87fdea4 --- /dev/null +++ b/app/soapbox/entity-store/hooks/useIncrementEntity.ts @@ -0,0 +1,33 @@ +import { useAppDispatch } from 'soapbox/hooks'; + +import { incrementEntities } from '../actions'; + +import { parseEntitiesPath } from './utils'; + +import type { ExpandedEntitiesPath } from './types'; + +type IncrementFn = (entityId: string) => Promise | T; + +/** + * Increases (or decreases) the `totalCount` in the entity list by the specified amount. + * This only works if the API returns an `X-Total-Count` header and your components read it. + */ +function useIncrementEntity( + expandedPath: ExpandedEntitiesPath, + diff: number, + incrementFn: IncrementFn, +) { + const { entityType, listKey } = parseEntitiesPath(expandedPath); + const dispatch = useAppDispatch(); + + return async function incrementEntity(entityId: string): Promise { + try { + await incrementFn(entityId); + dispatch(incrementEntities(entityType, listKey, diff)); + } catch (e) { + dispatch(incrementEntities(entityType, listKey, diff * -1)); + } + }; +} + +export { useIncrementEntity }; \ No newline at end of file diff --git a/app/soapbox/entity-store/reducer.ts b/app/soapbox/entity-store/reducer.ts index 7559b66a7..b71fb812f 100644 --- a/app/soapbox/entity-store/reducer.ts +++ b/app/soapbox/entity-store/reducer.ts @@ -9,6 +9,7 @@ import { ENTITIES_FETCH_FAIL, EntityAction, ENTITIES_INVALIDATE_LIST, + ENTITIES_INCREMENT, } from './actions'; import { createCache, createList, updateStore, updateList } from './utils'; @@ -108,6 +109,23 @@ const dismissEntities = ( }); }; +const incrementEntities = ( + state: State, + entityType: string, + listKey: string, + diff: number, +) => { + return produce(state, draft => { + const cache = draft[entityType] ?? createCache(); + const list = cache.lists[listKey]; + + if (typeof list?.state?.totalCount === 'number') { + list.state.totalCount += diff; + draft[entityType] = cache; + } + }); +}; + const setFetching = ( state: State, entityType: string, @@ -146,6 +164,8 @@ function reducer(state: Readonly = {}, action: EntityAction): State { return deleteEntities(state, action.entityType, action.ids, action.opts); case ENTITIES_DISMISS: return dismissEntities(state, action.entityType, action.ids, action.listKey); + case ENTITIES_INCREMENT: + return incrementEntities(state, action.entityType, action.listKey, action.diff); case ENTITIES_FETCH_SUCCESS: return importEntities(state, action.entityType, action.entities, action.listKey, action.newState, action.overwrite); case ENTITIES_FETCH_REQUEST: diff --git a/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts b/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts index cafea3601..560aef329 100644 --- a/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts +++ b/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts @@ -1,5 +1,5 @@ import { Entities } from 'soapbox/entity-store/entities'; -import { useEntities } from 'soapbox/entity-store/hooks'; +import { useEntities, useIncrementEntity } from 'soapbox/entity-store/hooks'; import { useApi } from 'soapbox/hooks/useApi'; import { accountSchema } from 'soapbox/schemas'; @@ -15,17 +15,17 @@ function useGroupMembershipRequests(groupId: string) { { schema: accountSchema }, ); - function authorize(accountId: string) { + const authorize = useIncrementEntity(path, -1, (accountId: string) => { return api .post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`) .then(invalidate); - } + }); - function reject(accountId: string) { + const reject = useIncrementEntity(path, -1, (accountId: string) => { return api .post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`) .then(invalidate); - } + }); return { accounts: entities,