Add useBatchedEntities hook for relationships
This commit is contained in:
parent
a5e213eca0
commit
989d99f908
|
@ -1,5 +1,6 @@
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||||
import { useApi } from 'soapbox/hooks/useApi';
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
import { type Account, accountSchema } from 'soapbox/schemas';
|
import { type Account, accountSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
@ -11,6 +12,8 @@ interface UseAccountOpts {
|
||||||
|
|
||||||
function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
|
function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
const { me } = useLoggedIn();
|
||||||
const { withRelationship } = opts;
|
const { withRelationship } = opts;
|
||||||
|
|
||||||
const { entity: account, ...result } = useEntity<Account>(
|
const { entity: account, ...result } = useEntity<Account>(
|
||||||
|
@ -24,10 +27,14 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
|
||||||
isLoading: isRelationshipLoading,
|
isLoading: isRelationshipLoading,
|
||||||
} = useRelationship(accountId, { enabled: withRelationship });
|
} = useRelationship(accountId, { enabled: withRelationship });
|
||||||
|
|
||||||
|
const isBlocked = account?.relationship?.blocked_by === true;
|
||||||
|
const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
isLoading: result.isLoading,
|
isLoading: result.isLoading,
|
||||||
isRelationshipLoading,
|
isRelationshipLoading,
|
||||||
|
isUnavailable,
|
||||||
account: account ? { ...account, relationship } : undefined,
|
account: account ? { ...account, relationship } : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntityLookup } from 'soapbox/entity-store/hooks';
|
import { useEntityLookup } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||||
import { useApi } from 'soapbox/hooks/useApi';
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
import { type Account, accountSchema } from 'soapbox/schemas';
|
import { type Account, accountSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
@ -11,6 +12,8 @@ interface UseAccountLookupOpts {
|
||||||
|
|
||||||
function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = {}) {
|
function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = {}) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
const features = useFeatures();
|
||||||
|
const { me } = useLoggedIn();
|
||||||
const { withRelationship } = opts;
|
const { withRelationship } = opts;
|
||||||
|
|
||||||
const { entity: account, ...result } = useEntityLookup<Account>(
|
const { entity: account, ...result } = useEntityLookup<Account>(
|
||||||
|
@ -25,10 +28,14 @@ function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts =
|
||||||
isLoading: isRelationshipLoading,
|
isLoading: isRelationshipLoading,
|
||||||
} = useRelationship(account?.id, { enabled: withRelationship });
|
} = useRelationship(account?.id, { enabled: withRelationship });
|
||||||
|
|
||||||
|
const isBlocked = account?.relationship?.blocked_by === true;
|
||||||
|
const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
isLoading: result.isLoading,
|
isLoading: result.isLoading,
|
||||||
isRelationshipLoading,
|
isRelationshipLoading,
|
||||||
|
isUnavailable,
|
||||||
account: account ? { ...account, relationship } : undefined,
|
account: account ? { ...account, relationship } : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { 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 { useLoggedIn } from 'soapbox/hooks';
|
||||||
import { useApi } from 'soapbox/hooks/useApi';
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
function useRelationships(ids: string[]) {
|
function useRelationships(listKey: string[], ids: string[]) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const { isLoggedIn } = useLoggedIn();
|
const { isLoggedIn } = useLoggedIn();
|
||||||
const q = ids.map(id => `id[]=${id}`).join('&');
|
const q = ids.map(id => `id[]=${id}`).join('&');
|
||||||
|
|
||||||
const { entities: relationships, ...result } = useEntities<Relationship>(
|
const { entityMap: relationships, ...result } = useBatchedEntities<Relationship>(
|
||||||
[Entities.RELATIONSHIPS, q],
|
[Entities.RELATIONSHIPS, ...listKey],
|
||||||
|
ids,
|
||||||
() => api.get(`/api/v1/accounts/relationships?${q}`),
|
() => api.get(`/api/v1/accounts/relationships?${q}`),
|
||||||
{ schema: relationshipSchema, enabled: isLoggedIn && ids.filter(Boolean).length > 0 },
|
{ schema: relationshipSchema, enabled: isLoggedIn },
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return { relationships, ...result };
|
||||||
...result,
|
|
||||||
relationships,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useRelationships };
|
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 };
|
|
@ -7,12 +7,12 @@ import { filteredArray } from 'soapbox/schemas/utils';
|
||||||
import { realNumberSchema } from 'soapbox/utils/numbers';
|
import { realNumberSchema } from 'soapbox/utils/numbers';
|
||||||
|
|
||||||
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess, invalidateEntityList } from '../actions';
|
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess, invalidateEntityList } from '../actions';
|
||||||
|
import { selectEntities, selectListState, useListState } from '../selectors';
|
||||||
|
|
||||||
import { parseEntitiesPath } from './utils';
|
import { parseEntitiesPath } from './utils';
|
||||||
|
|
||||||
import type { Entity, EntityListState } from '../types';
|
import type { Entity } from '../types';
|
||||||
import type { EntitiesPath, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
|
import type { EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
|
||||||
import type { RootState } from 'soapbox/store';
|
|
||||||
|
|
||||||
/** Additional options for the hook. */
|
/** Additional options for the hook. */
|
||||||
interface UseEntitiesOpts<TEntity extends Entity> {
|
interface UseEntitiesOpts<TEntity extends Entity> {
|
||||||
|
@ -42,6 +42,7 @@ function useEntities<TEntity extends Entity>(
|
||||||
|
|
||||||
const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
|
const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
|
||||||
const entities = useAppSelector(state => selectEntities<TEntity>(state, path));
|
const entities = useAppSelector(state => selectEntities<TEntity>(state, path));
|
||||||
|
const schema = opts.schema || z.custom<TEntity>();
|
||||||
|
|
||||||
const isEnabled = opts.enabled ?? true;
|
const isEnabled = opts.enabled ?? true;
|
||||||
const isFetching = useListState(path, 'fetching');
|
const isFetching = useListState(path, 'fetching');
|
||||||
|
@ -62,7 +63,6 @@ function useEntities<TEntity extends Entity>(
|
||||||
dispatch(entitiesFetchRequest(entityType, listKey));
|
dispatch(entitiesFetchRequest(entityType, listKey));
|
||||||
try {
|
try {
|
||||||
const response = await req();
|
const response = await req();
|
||||||
const schema = opts.schema || z.custom<TEntity>();
|
|
||||||
const entities = filteredArray(schema).parse(response.data);
|
const entities = filteredArray(schema).parse(response.data);
|
||||||
const parsedCount = realNumberSchema.safeParse(response.headers['x-total-count']);
|
const parsedCount = realNumberSchema.safeParse(response.headers['x-total-count']);
|
||||||
const totalCount = parsedCount.success ? parsedCount.data : undefined;
|
const totalCount = parsedCount.success ? parsedCount.data : undefined;
|
||||||
|
@ -133,46 +133,6 @@ function useEntities<TEntity extends Entity>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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 {
|
export {
|
||||||
useEntities,
|
useEntities,
|
||||||
};
|
};
|
|
@ -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,
|
||||||
|
};
|
|
@ -8,7 +8,7 @@ import { useAccountLookup } from 'soapbox/api/hooks';
|
||||||
import LoadMore from 'soapbox/components/load-more';
|
import LoadMore from 'soapbox/components/load-more';
|
||||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||||
import { Column, Spinner } from 'soapbox/components/ui';
|
import { Column, Spinner } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures, useLoggedIn } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
import { getAccountGallery } from 'soapbox/selectors';
|
import { getAccountGallery } from 'soapbox/selectors';
|
||||||
|
|
||||||
import MediaItem from './components/media-item';
|
import MediaItem from './components/media-item';
|
||||||
|
@ -34,17 +34,13 @@ const LoadMoreMedia: React.FC<ILoadMoreMedia> = ({ maxId, onLoadMore }) => {
|
||||||
const AccountGallery = () => {
|
const AccountGallery = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { username } = useParams<{ username: string }>();
|
const { username } = useParams<{ username: string }>();
|
||||||
const features = useFeatures();
|
|
||||||
const { me } = useLoggedIn();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
account,
|
account,
|
||||||
isLoading: accountLoading,
|
isLoading: accountLoading,
|
||||||
|
isUnavailable,
|
||||||
} = useAccountLookup(username, { withRelationship: true });
|
} = useAccountLookup(username, { withRelationship: true });
|
||||||
|
|
||||||
const isBlocked = account?.relationship?.blocked_by === true;
|
|
||||||
const unavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible);
|
|
||||||
|
|
||||||
const attachments: ImmutableList<Attachment> = useAppSelector((state) => getAccountGallery(state, account!.id));
|
const attachments: ImmutableList<Attachment> = useAppSelector((state) => getAccountGallery(state, account!.id));
|
||||||
const isLoading = useAppSelector((state) => state.timelines.get(`account:${account?.id}:media`)?.isLoading);
|
const isLoading = useAppSelector((state) => state.timelines.get(`account:${account?.id}:media`)?.isLoading);
|
||||||
const hasMore = useAppSelector((state) => state.timelines.get(`account:${account?.id}:media`)?.hasMore);
|
const hasMore = useAppSelector((state) => state.timelines.get(`account:${account?.id}:media`)?.hasMore);
|
||||||
|
@ -106,7 +102,7 @@ const AccountGallery = () => {
|
||||||
loadOlder = <LoadMore className='my-auto' visible={!isLoading} onClick={handleLoadOlder} />;
|
loadOlder = <LoadMore className='my-auto' visible={!isLoading} onClick={handleLoadOlder} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unavailable) {
|
if (isUnavailable) {
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<div className='empty-column-indicator'>
|
<div className='empty-column-indicator'>
|
||||||
|
|
|
@ -1,20 +1,12 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import React from 'react';
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import {
|
|
||||||
fetchAccount,
|
|
||||||
fetchFollowing,
|
|
||||||
expandFollowing,
|
|
||||||
fetchAccountByUsername,
|
|
||||||
} from 'soapbox/actions/accounts';
|
|
||||||
import { useAccountLookup } from 'soapbox/api/hooks';
|
import { useAccountLookup } from 'soapbox/api/hooks';
|
||||||
|
import { useFollowing } from 'soapbox/api/hooks/accounts/useFollowing';
|
||||||
|
import Account from 'soapbox/components/account';
|
||||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||||
import { Column, Spinner } from 'soapbox/components/ui';
|
import { Column, Spinner } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
|
||||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.following', defaultMessage: 'Following' },
|
heading: { id: 'column.following', defaultMessage: 'Following' },
|
||||||
|
@ -27,53 +19,19 @@ interface IFollowing {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Displays a list of accounts the given user is following. */
|
/** Displays a list of accounts the given user is following. */
|
||||||
const Following: React.FC<IFollowing> = (props) => {
|
const Following: React.FC<IFollowing> = ({ params }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const features = useFeatures();
|
|
||||||
const { account: ownAccount } = useOwnAccount();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const { account, isUnavailable } = useAccountLookup(params?.username);
|
||||||
|
|
||||||
const username = props.params?.username || '';
|
const {
|
||||||
const { account } = useAccountLookup(username);
|
accounts,
|
||||||
const isOwnAccount = username.toLowerCase() === ownAccount?.username?.toLowerCase();
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
isLoading,
|
||||||
|
} = useFollowing(account?.id);
|
||||||
|
|
||||||
const accountIds = useAppSelector(state => state.user_lists.following.get(account!?.id)?.items || ImmutableOrderedSet<string>());
|
if (isLoading) {
|
||||||
const hasMore = useAppSelector(state => !!state.user_lists.following.get(account!?.id)?.next);
|
|
||||||
|
|
||||||
const isUnavailable = useAppSelector(state => {
|
|
||||||
const blockedBy = state.relationships.getIn([account?.id, 'blocked_by']) === true;
|
|
||||||
return isOwnAccount ? false : (blockedBy && !features.blockersVisible);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleLoadMore = useCallback(debounce(() => {
|
|
||||||
if (account) {
|
|
||||||
dispatch(expandFollowing(account.id));
|
|
||||||
}
|
|
||||||
}, 300, { leading: true }), [account?.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let promises = [];
|
|
||||||
|
|
||||||
if (account) {
|
|
||||||
promises = [
|
|
||||||
dispatch(fetchAccount(account.id)),
|
|
||||||
dispatch(fetchFollowing(account.id)),
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
promises = [
|
|
||||||
dispatch(fetchAccountByUsername(username)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(promises)
|
|
||||||
.then(() => setLoading(false))
|
|
||||||
.catch(() => setLoading(false));
|
|
||||||
|
|
||||||
}, [account?.id, username]);
|
|
||||||
|
|
||||||
if (loading && accountIds.isEmpty()) {
|
|
||||||
return (
|
return (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
);
|
);
|
||||||
|
@ -97,14 +55,14 @@ const Following: React.FC<IFollowing> = (props) => {
|
||||||
<Column label={intl.formatMessage(messages.heading)} transparent>
|
<Column label={intl.formatMessage(messages.heading)} transparent>
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='following'
|
scrollKey='following'
|
||||||
hasMore={hasMore}
|
hasMore={hasNextPage}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={fetchNextPage}
|
||||||
emptyMessage={<FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />}
|
emptyMessage={<FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />}
|
||||||
itemClassName='pb-4'
|
itemClassName='pb-4'
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accounts.map((account) => (
|
||||||
<AccountContainer key={id} id={id} />,
|
<Account key={account.id} account={account} />
|
||||||
)}
|
))}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue