Add useTransaction hook
This commit is contained in:
parent
2657c8f946
commit
9f53a81fa1
7 changed files with 130 additions and 35 deletions
|
@ -1,13 +1,9 @@
|
|||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useChangeEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useLoggedIn } from 'soapbox/hooks';
|
||||
import { useTransaction } from 'soapbox/entity-store/hooks';
|
||||
import { useAppDispatch, useLoggedIn } from 'soapbox/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { type Account } from 'soapbox/schemas';
|
||||
|
||||
function useChangeAccount() {
|
||||
const { changeEntity: changeAccount } = useChangeEntity<Account>(Entities.ACCOUNTS);
|
||||
return { changeAccount };
|
||||
}
|
||||
import { relationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
interface FollowOpts {
|
||||
reblogs?: boolean
|
||||
|
@ -17,50 +13,75 @@ interface FollowOpts {
|
|||
|
||||
function useFollow() {
|
||||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const { changeAccount } = useChangeAccount();
|
||||
const { transaction } = useTransaction();
|
||||
|
||||
function incrementFollowers(accountId: string) {
|
||||
changeAccount(accountId, (account) => ({
|
||||
...account,
|
||||
followers_count: account.followers_count + 1,
|
||||
}));
|
||||
function followEffect(accountId: string) {
|
||||
transaction({
|
||||
Accounts: {
|
||||
[accountId]: (account) => ({
|
||||
...account,
|
||||
followers_count: account.followers_count + 1,
|
||||
}),
|
||||
},
|
||||
Relationships: {
|
||||
[accountId]: (relationship) => ({
|
||||
...relationship,
|
||||
following: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function decrementFollowers(accountId: string) {
|
||||
changeAccount(accountId, (account) => ({
|
||||
...account,
|
||||
followers_count: Math.max(0, account.followers_count - 1),
|
||||
}));
|
||||
function unfollowEffect(accountId: string) {
|
||||
transaction({
|
||||
Accounts: {
|
||||
[accountId]: (account) => ({
|
||||
...account,
|
||||
followers_count: Math.max(0, account.followers_count - 1),
|
||||
}),
|
||||
},
|
||||
Relationships: {
|
||||
[accountId]: (relationship) => ({
|
||||
...relationship,
|
||||
following: false,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function follow(accountId: string, options: FollowOpts = {}) {
|
||||
if (!isLoggedIn) return;
|
||||
incrementFollowers(accountId);
|
||||
followEffect(accountId);
|
||||
|
||||
try {
|
||||
await api.post(`/api/v1/accounts/${accountId}/follow`, options);
|
||||
const response = await api.post(`/api/v1/accounts/${accountId}/follow`, options);
|
||||
const result = relationshipSchema.safeParse(response.data);
|
||||
if (result.success) {
|
||||
dispatch(importEntities([result.data], Entities.RELATIONSHIPS));
|
||||
}
|
||||
} catch (e) {
|
||||
decrementFollowers(accountId);
|
||||
unfollowEffect(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
async function unfollow(accountId: string) {
|
||||
if (!isLoggedIn) return;
|
||||
decrementFollowers(accountId);
|
||||
unfollowEffect(accountId);
|
||||
|
||||
try {
|
||||
await api.post(`/api/v1/accounts/${accountId}/unfollow`);
|
||||
} catch (e) {
|
||||
incrementFollowers(accountId);
|
||||
followEffect(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
follow,
|
||||
unfollow,
|
||||
incrementFollowers,
|
||||
decrementFollowers,
|
||||
followEffect,
|
||||
unfollowEffect,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Entity, EntityListState, ImportPosition } from './types';
|
||||
import type { EntitiesTransaction, Entity, EntityListState, ImportPosition } from './types';
|
||||
|
||||
const ENTITIES_IMPORT = 'ENTITIES_IMPORT' as const;
|
||||
const ENTITIES_DELETE = 'ENTITIES_DELETE' as const;
|
||||
|
@ -8,6 +8,7 @@ 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. */
|
||||
function importEntities(entities: Entity[], entityType: string, listKey?: string, pos?: ImportPosition) {
|
||||
|
@ -95,6 +96,13 @@ function invalidateEntityList(entityType: string, listKey: string) {
|
|||
};
|
||||
}
|
||||
|
||||
function entitiesTransaction(transaction: EntitiesTransaction) {
|
||||
return {
|
||||
type: ENTITIES_TRANSACTION,
|
||||
transaction,
|
||||
};
|
||||
}
|
||||
|
||||
/** Any action pertaining to entities. */
|
||||
type EntityAction =
|
||||
ReturnType<typeof importEntities>
|
||||
|
@ -104,7 +112,8 @@ type EntityAction =
|
|||
| ReturnType<typeof entitiesFetchRequest>
|
||||
| ReturnType<typeof entitiesFetchSuccess>
|
||||
| ReturnType<typeof entitiesFetchFail>
|
||||
| ReturnType<typeof invalidateEntityList>;
|
||||
| ReturnType<typeof invalidateEntityList>
|
||||
| ReturnType<typeof entitiesTransaction>;
|
||||
|
||||
export {
|
||||
ENTITIES_IMPORT,
|
||||
|
@ -115,6 +124,7 @@ export {
|
|||
ENTITIES_FETCH_SUCCESS,
|
||||
ENTITIES_FETCH_FAIL,
|
||||
ENTITIES_INVALIDATE_LIST,
|
||||
ENTITIES_TRANSACTION,
|
||||
importEntities,
|
||||
deleteEntities,
|
||||
dismissEntities,
|
||||
|
@ -123,7 +133,7 @@ export {
|
|||
entitiesFetchSuccess,
|
||||
entitiesFetchFail,
|
||||
invalidateEntityList,
|
||||
EntityAction,
|
||||
entitiesTransaction,
|
||||
};
|
||||
|
||||
export type { DeleteEntitiesOpts };
|
||||
export type { DeleteEntitiesOpts, EntityAction };
|
|
@ -1,4 +1,6 @@
|
|||
export enum Entities {
|
||||
import type * as Schemas from 'soapbox/schemas';
|
||||
|
||||
enum Entities {
|
||||
ACCOUNTS = 'Accounts',
|
||||
GROUPS = 'Groups',
|
||||
GROUP_MEMBERSHIPS = 'GroupMemberships',
|
||||
|
@ -8,3 +10,16 @@ export enum Entities {
|
|||
RELATIONSHIPS = 'Relationships',
|
||||
STATUSES = 'Statuses'
|
||||
}
|
||||
|
||||
interface EntityTypes {
|
||||
[Entities.ACCOUNTS]: Schemas.Account
|
||||
[Entities.GROUPS]: Schemas.Group
|
||||
[Entities.GROUP_MEMBERSHIPS]: Schemas.GroupMember
|
||||
[Entities.GROUP_RELATIONSHIPS]: Schemas.GroupRelationship
|
||||
[Entities.GROUP_TAGS]: Schemas.GroupTag
|
||||
[Entities.PATRON_USERS]: Schemas.PatronUser
|
||||
[Entities.RELATIONSHIPS]: Schemas.Relationship
|
||||
[Entities.STATUSES]: Schemas.Status
|
||||
}
|
||||
|
||||
export { Entities, type EntityTypes };
|
|
@ -7,3 +7,4 @@ export { useDeleteEntity } from './useDeleteEntity';
|
|||
export { useDismissEntity } from './useDismissEntity';
|
||||
export { useIncrementEntity } from './useIncrementEntity';
|
||||
export { useChangeEntity } from './useChangeEntity';
|
||||
export { useTransaction } from './useTransaction';
|
23
app/soapbox/entity-store/hooks/useTransaction.ts
Normal file
23
app/soapbox/entity-store/hooks/useTransaction.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { entitiesTransaction } from 'soapbox/entity-store/actions';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import type { EntityTypes } from 'soapbox/entity-store/entities';
|
||||
import type { EntitiesTransaction, Entity } from 'soapbox/entity-store/types';
|
||||
|
||||
type Updater<TEntity extends Entity> = Record<string, (entity: TEntity) => TEntity>
|
||||
|
||||
type Changes = Partial<{
|
||||
[K in keyof EntityTypes]: Updater<EntityTypes[K]>
|
||||
}>
|
||||
|
||||
function useTransaction() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function transaction(changes: Changes): void {
|
||||
dispatch(entitiesTransaction(changes as EntitiesTransaction));
|
||||
}
|
||||
|
||||
return { transaction };
|
||||
}
|
||||
|
||||
export { useTransaction };
|
|
@ -10,11 +10,12 @@ import {
|
|||
EntityAction,
|
||||
ENTITIES_INVALIDATE_LIST,
|
||||
ENTITIES_INCREMENT,
|
||||
ENTITIES_TRANSACTION,
|
||||
} from './actions';
|
||||
import { createCache, createList, updateStore, updateList } from './utils';
|
||||
|
||||
import type { DeleteEntitiesOpts } from './actions';
|
||||
import type { Entity, EntityCache, EntityListState, ImportPosition } from './types';
|
||||
import type { EntitiesTransaction, Entity, EntityCache, EntityListState, ImportPosition } from './types';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
|
@ -156,6 +157,20 @@ const invalidateEntityList = (state: State, entityType: string, listKey: string)
|
|||
});
|
||||
};
|
||||
|
||||
const doTransaction = (state: State, transaction: EntitiesTransaction) => {
|
||||
return produce(state, draft => {
|
||||
for (const [entityType, changes] of Object.entries(transaction)) {
|
||||
const cache = draft[entityType] ?? createCache();
|
||||
for (const [id, change] of Object.entries(changes)) {
|
||||
const entity = cache.store[id];
|
||||
if (entity) {
|
||||
cache.store[id] = change(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** Stores various entity data and lists in a one reducer. */
|
||||
function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
||||
switch (action.type) {
|
||||
|
@ -175,6 +190,8 @@ function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
|||
return setFetching(state, action.entityType, action.listKey, false, action.error);
|
||||
case ENTITIES_INVALIDATE_LIST:
|
||||
return invalidateEntityList(state, action.entityType, action.listKey);
|
||||
case ENTITIES_TRANSACTION:
|
||||
return doTransaction(state, action.transaction);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -50,11 +50,19 @@ interface EntityCache<TEntity extends Entity = Entity> {
|
|||
/** Whether to import items at the start or end of the list. */
|
||||
type ImportPosition = 'start' | 'end'
|
||||
|
||||
export {
|
||||
/** Map of entity mutation functions to perform at once on the store. */
|
||||
interface EntitiesTransaction {
|
||||
[entityType: string]: {
|
||||
[entityId: string]: <TEntity extends Entity>(entity: TEntity) => TEntity
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
Entity,
|
||||
EntityStore,
|
||||
EntityList,
|
||||
EntityListState,
|
||||
EntityCache,
|
||||
ImportPosition,
|
||||
EntitiesTransaction,
|
||||
};
|
Loading…
Reference in a new issue