kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Add useTransaction hook
rodzic
2657c8f946
commit
9f53a81fa1
|
@ -1,13 +1,9 @@
|
||||||
|
import { importEntities } from 'soapbox/entity-store/actions';
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useChangeEntity } from 'soapbox/entity-store/hooks';
|
import { useTransaction } from 'soapbox/entity-store/hooks';
|
||||||
import { useLoggedIn } from 'soapbox/hooks';
|
import { useAppDispatch, useLoggedIn } from 'soapbox/hooks';
|
||||||
import { useApi } from 'soapbox/hooks/useApi';
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
import { type Account } from 'soapbox/schemas';
|
import { relationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
function useChangeAccount() {
|
|
||||||
const { changeEntity: changeAccount } = useChangeEntity<Account>(Entities.ACCOUNTS);
|
|
||||||
return { changeAccount };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FollowOpts {
|
interface FollowOpts {
|
||||||
reblogs?: boolean
|
reblogs?: boolean
|
||||||
|
@ -17,50 +13,75 @@ interface FollowOpts {
|
||||||
|
|
||||||
function useFollow() {
|
function useFollow() {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const { isLoggedIn } = useLoggedIn();
|
const { isLoggedIn } = useLoggedIn();
|
||||||
const { changeAccount } = useChangeAccount();
|
const { transaction } = useTransaction();
|
||||||
|
|
||||||
function incrementFollowers(accountId: string) {
|
function followEffect(accountId: string) {
|
||||||
changeAccount(accountId, (account) => ({
|
transaction({
|
||||||
...account,
|
Accounts: {
|
||||||
followers_count: account.followers_count + 1,
|
[accountId]: (account) => ({
|
||||||
}));
|
...account,
|
||||||
|
followers_count: account.followers_count + 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Relationships: {
|
||||||
|
[accountId]: (relationship) => ({
|
||||||
|
...relationship,
|
||||||
|
following: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrementFollowers(accountId: string) {
|
function unfollowEffect(accountId: string) {
|
||||||
changeAccount(accountId, (account) => ({
|
transaction({
|
||||||
...account,
|
Accounts: {
|
||||||
followers_count: Math.max(0, account.followers_count - 1),
|
[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 = {}) {
|
async function follow(accountId: string, options: FollowOpts = {}) {
|
||||||
if (!isLoggedIn) return;
|
if (!isLoggedIn) return;
|
||||||
incrementFollowers(accountId);
|
followEffect(accountId);
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
decrementFollowers(accountId);
|
unfollowEffect(accountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unfollow(accountId: string) {
|
async function unfollow(accountId: string) {
|
||||||
if (!isLoggedIn) return;
|
if (!isLoggedIn) return;
|
||||||
decrementFollowers(accountId);
|
unfollowEffect(accountId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post(`/api/v1/accounts/${accountId}/unfollow`);
|
await api.post(`/api/v1/accounts/${accountId}/unfollow`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
incrementFollowers(accountId);
|
followEffect(accountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
follow,
|
follow,
|
||||||
unfollow,
|
unfollow,
|
||||||
incrementFollowers,
|
followEffect,
|
||||||
decrementFollowers,
|
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_IMPORT = 'ENTITIES_IMPORT' as const;
|
||||||
const ENTITIES_DELETE = 'ENTITIES_DELETE' 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_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const;
|
||||||
const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const;
|
const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const;
|
||||||
const ENTITIES_INVALIDATE_LIST = 'ENTITIES_INVALIDATE_LIST' 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. */
|
/** Action to import entities into the cache. */
|
||||||
function importEntities(entities: Entity[], entityType: string, listKey?: string, pos?: ImportPosition) {
|
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. */
|
/** Any action pertaining to entities. */
|
||||||
type EntityAction =
|
type EntityAction =
|
||||||
ReturnType<typeof importEntities>
|
ReturnType<typeof importEntities>
|
||||||
|
@ -104,7 +112,8 @@ type EntityAction =
|
||||||
| ReturnType<typeof entitiesFetchRequest>
|
| ReturnType<typeof entitiesFetchRequest>
|
||||||
| ReturnType<typeof entitiesFetchSuccess>
|
| ReturnType<typeof entitiesFetchSuccess>
|
||||||
| ReturnType<typeof entitiesFetchFail>
|
| ReturnType<typeof entitiesFetchFail>
|
||||||
| ReturnType<typeof invalidateEntityList>;
|
| ReturnType<typeof invalidateEntityList>
|
||||||
|
| ReturnType<typeof entitiesTransaction>;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ENTITIES_IMPORT,
|
ENTITIES_IMPORT,
|
||||||
|
@ -115,6 +124,7 @@ export {
|
||||||
ENTITIES_FETCH_SUCCESS,
|
ENTITIES_FETCH_SUCCESS,
|
||||||
ENTITIES_FETCH_FAIL,
|
ENTITIES_FETCH_FAIL,
|
||||||
ENTITIES_INVALIDATE_LIST,
|
ENTITIES_INVALIDATE_LIST,
|
||||||
|
ENTITIES_TRANSACTION,
|
||||||
importEntities,
|
importEntities,
|
||||||
deleteEntities,
|
deleteEntities,
|
||||||
dismissEntities,
|
dismissEntities,
|
||||||
|
@ -123,7 +133,7 @@ export {
|
||||||
entitiesFetchSuccess,
|
entitiesFetchSuccess,
|
||||||
entitiesFetchFail,
|
entitiesFetchFail,
|
||||||
invalidateEntityList,
|
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',
|
ACCOUNTS = 'Accounts',
|
||||||
GROUPS = 'Groups',
|
GROUPS = 'Groups',
|
||||||
GROUP_MEMBERSHIPS = 'GroupMemberships',
|
GROUP_MEMBERSHIPS = 'GroupMemberships',
|
||||||
|
@ -7,4 +9,17 @@ export enum Entities {
|
||||||
PATRON_USERS = 'PatronUsers',
|
PATRON_USERS = 'PatronUsers',
|
||||||
RELATIONSHIPS = 'Relationships',
|
RELATIONSHIPS = 'Relationships',
|
||||||
STATUSES = 'Statuses'
|
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 };
|
|
@ -6,4 +6,5 @@ export { useCreateEntity } from './useCreateEntity';
|
||||||
export { useDeleteEntity } from './useDeleteEntity';
|
export { useDeleteEntity } from './useDeleteEntity';
|
||||||
export { useDismissEntity } from './useDismissEntity';
|
export { useDismissEntity } from './useDismissEntity';
|
||||||
export { useIncrementEntity } from './useIncrementEntity';
|
export { useIncrementEntity } from './useIncrementEntity';
|
||||||
export { useChangeEntity } from './useChangeEntity';
|
export { useChangeEntity } from './useChangeEntity';
|
||||||
|
export { useTransaction } from './useTransaction';
|
|
@ -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,
|
EntityAction,
|
||||||
ENTITIES_INVALIDATE_LIST,
|
ENTITIES_INVALIDATE_LIST,
|
||||||
ENTITIES_INCREMENT,
|
ENTITIES_INCREMENT,
|
||||||
|
ENTITIES_TRANSACTION,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { createCache, createList, updateStore, updateList } from './utils';
|
import { createCache, createList, updateStore, updateList } from './utils';
|
||||||
|
|
||||||
import type { DeleteEntitiesOpts } from './actions';
|
import type { DeleteEntitiesOpts } from './actions';
|
||||||
import type { Entity, EntityCache, EntityListState, ImportPosition } from './types';
|
import type { EntitiesTransaction, Entity, EntityCache, EntityListState, ImportPosition } from './types';
|
||||||
|
|
||||||
enableMapSet();
|
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. */
|
/** Stores various entity data and lists in a one reducer. */
|
||||||
function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
||||||
switch (action.type) {
|
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);
|
return setFetching(state, action.entityType, action.listKey, false, action.error);
|
||||||
case ENTITIES_INVALIDATE_LIST:
|
case ENTITIES_INVALIDATE_LIST:
|
||||||
return invalidateEntityList(state, action.entityType, action.listKey);
|
return invalidateEntityList(state, action.entityType, action.listKey);
|
||||||
|
case ENTITIES_TRANSACTION:
|
||||||
|
return doTransaction(state, action.transaction);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,11 +50,19 @@ interface EntityCache<TEntity extends Entity = Entity> {
|
||||||
/** Whether to import items at the start or end of the list. */
|
/** Whether to import items at the start or end of the list. */
|
||||||
type ImportPosition = 'start' | 'end'
|
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,
|
Entity,
|
||||||
EntityStore,
|
EntityStore,
|
||||||
EntityList,
|
EntityList,
|
||||||
EntityListState,
|
EntityListState,
|
||||||
EntityCache,
|
EntityCache,
|
||||||
ImportPosition,
|
ImportPosition,
|
||||||
|
EntitiesTransaction,
|
||||||
};
|
};
|
Ładowanie…
Reference in New Issue