environments/review-change-ent-6o2wvy/deployments/3529
parent
a5e213eca0
commit
989d99f908
@ -0,0 +1,30 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { Account, accountSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useRelationships } from './useRelationships';
|
||||
|
||||
function useFollowing(accountId: string | undefined) {
|
||||
const api = useApi();
|
||||
|
||||
const { entities, ...rest } = useEntities(
|
||||
[Entities.ACCOUNTS, accountId!, 'following'],
|
||||
() => api.get(`/api/v1/accounts/${accountId}/following`),
|
||||
{ schema: accountSchema, enabled: !!accountId },
|
||||
);
|
||||
|
||||
const { relationships } = useRelationships(
|
||||
[accountId!, 'following'],
|
||||
entities.map(({ id }) => id),
|
||||
);
|
||||
|
||||
const accounts: Account[] = entities.map((account) => ({
|
||||
...account,
|
||||
relationship: relationships[account.id],
|
||||
}));
|
||||
|
||||
return { accounts, ...rest };
|
||||
}
|
||||
|
||||
export { useFollowing };
|
@ -1,24 +1,22 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities';
|
||||
import { useLoggedIn } from 'soapbox/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
function useRelationships(ids: string[]) {
|
||||
function useRelationships(listKey: string[], ids: string[]) {
|
||||
const api = useApi();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const q = ids.map(id => `id[]=${id}`).join('&');
|
||||
|
||||
const { entities: relationships, ...result } = useEntities<Relationship>(
|
||||
[Entities.RELATIONSHIPS, q],
|
||||
const { entityMap: relationships, ...result } = useBatchedEntities<Relationship>(
|
||||
[Entities.RELATIONSHIPS, ...listKey],
|
||||
ids,
|
||||
() => api.get(`/api/v1/accounts/relationships?${q}`),
|
||||
{ schema: relationshipSchema, enabled: isLoggedIn && ids.filter(Boolean).length > 0 },
|
||||
{ schema: relationshipSchema, enabled: isLoggedIn },
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
relationships,
|
||||
};
|
||||
return { relationships, ...result };
|
||||
}
|
||||
|
||||
export { useRelationships };
|
@ -0,0 +1,103 @@
|
||||
import { useEffect } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAppDispatch, useAppSelector, useGetState } from 'soapbox/hooks';
|
||||
import { filteredArray } from 'soapbox/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 'soapbox/store';
|
||||
|
||||
interface UseBatchedEntitiesOpts<TEntity extends Entity> {
|
||||
schema?: EntitySchema<TEntity>
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
function 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 || z.custom<TEntity>();
|
||||
|
||||
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));
|
||||
|
||||
async function fetchEntities() {
|
||||
const isFetching = selectListState(getState(), path, 'fetching');
|
||||
if (isFetching) return;
|
||||
|
||||
dispatch(entitiesFetchRequest(entityType, listKey));
|
||||
try {
|
||||
const response = await entityFn(filteredIds);
|
||||
const entities = filteredArray(schema).parse(response.data);
|
||||
dispatch(entitiesFetchSuccess(entities, entityType, listKey, 'end', {
|
||||
next: undefined,
|
||||
prev: undefined,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function 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 };
|
@ -0,0 +1,53 @@
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { EntitiesPath } from './hooks/types';
|
||||
import type { Entity, EntityListState } from './types';
|
||||
import type { RootState } from 'soapbox/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. */
|
||||
function 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. */
|
||||
function useListState<K extends keyof EntityListState>(path: EntitiesPath, key: K) {
|
||||
return useAppSelector(state => selectListState(state, path, key));
|
||||
}
|
||||
|
||||
/** Get list of entities from Redux. */
|
||||
function 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;
|
||||
}, [])
|
||||
) : [];
|
||||
}
|
||||
|
||||
export {
|
||||
selectCache,
|
||||
selectList,
|
||||
selectListState,
|
||||
useListState,
|
||||
selectEntities,
|
||||
};
|
Loading…
Reference in new issue