EntityStore: incorporate Notifications, go back to using POJOs instead of Maps
This commit is contained in:
parent
f7bfc40b70
commit
27500193d8
7 changed files with 70 additions and 38 deletions
|
@ -13,14 +13,14 @@ function useEntities<TEntity extends Entity>(path: EntityPath, endpoint: string)
|
|||
|
||||
const [entityType, listKey] = path;
|
||||
|
||||
const cache = useAppSelector(state => state.entities.get(entityType));
|
||||
const list = cache?.lists.get(listKey);
|
||||
const cache = useAppSelector(state => state.entities[entityType]);
|
||||
const list = cache?.lists[listKey];
|
||||
|
||||
const entityIds = list?.ids;
|
||||
|
||||
const entities: readonly TEntity[] = entityIds ? (
|
||||
Array.from(entityIds).reduce<TEntity[]>((result, id) => {
|
||||
const entity = cache?.store.get(id) as TEntity | undefined;
|
||||
const entity = cache?.store[id] as TEntity | undefined;
|
||||
if (entity) {
|
||||
result.push(entity);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ function useEntity<TEntity extends Entity>(path: EntityPath, endpoint: string) {
|
|||
const dispatch = useAppDispatch();
|
||||
|
||||
const [entityType, entityId] = path;
|
||||
const entity = useAppSelector(state => state.entities.get(entityType)?.store.get(entityId)) as TEntity | undefined;
|
||||
const entity = useAppSelector(state => state.entities[entityType]?.store[entityId]) as TEntity | undefined;
|
||||
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const isLoading = isFetching && !entity;
|
||||
|
|
|
@ -14,30 +14,32 @@ import type { Entity, EntityCache, EntityListState } from './types';
|
|||
enableMapSet();
|
||||
|
||||
/** Entity reducer state. */
|
||||
type State = Map<string, EntityCache>;
|
||||
interface State {
|
||||
[entityType: string]: EntityCache | undefined
|
||||
}
|
||||
|
||||
/** Import entities into the cache. */
|
||||
const importEntities = (
|
||||
state: Readonly<State>,
|
||||
state: State,
|
||||
entityType: string,
|
||||
entities: Entity[],
|
||||
listKey?: string,
|
||||
newState?: EntityListState,
|
||||
): State => {
|
||||
return produce(state, draft => {
|
||||
const cache = draft.get(entityType) ?? createCache();
|
||||
const cache = draft[entityType] ?? createCache();
|
||||
cache.store = updateStore(cache.store, entities);
|
||||
|
||||
if (listKey) {
|
||||
let list = cache.lists.get(listKey) ?? createList();
|
||||
if (typeof listKey === 'string') {
|
||||
let list = { ...(cache.lists[listKey] ?? createList()) };
|
||||
list = updateList(list, entities);
|
||||
if (newState) {
|
||||
list.state = newState;
|
||||
}
|
||||
cache.lists.set(listKey, list);
|
||||
cache.lists[listKey] = list;
|
||||
}
|
||||
|
||||
return draft.set(entityType, cache);
|
||||
draft[entityType] = cache;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -48,20 +50,20 @@ const setFetching = (
|
|||
isFetching: boolean,
|
||||
) => {
|
||||
return produce(state, draft => {
|
||||
const cache = draft.get(entityType) ?? createCache();
|
||||
const cache = draft[entityType] ?? createCache();
|
||||
|
||||
if (listKey) {
|
||||
const list = cache.lists.get(listKey) ?? createList();
|
||||
if (typeof listKey === 'string') {
|
||||
const list = cache.lists[listKey] ?? createList();
|
||||
list.state.fetching = isFetching;
|
||||
cache.lists.set(listKey, list);
|
||||
cache.lists[listKey] = list;
|
||||
}
|
||||
|
||||
return draft.set(entityType, cache);
|
||||
draft[entityType] = cache;
|
||||
});
|
||||
};
|
||||
|
||||
/** Stores various entity data and lists in a one reducer. */
|
||||
function reducer(state: Readonly<State> = new Map(), action: EntityAction): State {
|
||||
function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
||||
switch (action.type) {
|
||||
case ENTITIES_IMPORT:
|
||||
return importEntities(state, action.entityType, action.entities, action.listKey);
|
||||
|
|
|
@ -5,7 +5,9 @@ interface Entity {
|
|||
}
|
||||
|
||||
/** Store of entities by ID. */
|
||||
type EntityStore = Map<string, Entity>
|
||||
interface EntityStore {
|
||||
[id: string]: Entity | undefined
|
||||
}
|
||||
|
||||
/** List of entity IDs and fetch state. */
|
||||
interface EntityList {
|
||||
|
@ -32,7 +34,9 @@ interface EntityCache {
|
|||
/** Map of entities of this type. */
|
||||
store: EntityStore
|
||||
/** Lists of entity IDs for a particular purpose. */
|
||||
lists: Map<string, EntityList>
|
||||
lists: {
|
||||
[listKey: string]: EntityList | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -3,8 +3,9 @@ import type { Entity, EntityStore, EntityList, EntityCache } from './types';
|
|||
/** Insert the entities into the store. */
|
||||
const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => {
|
||||
return entities.reduce<EntityStore>((store, entity) => {
|
||||
return store.set(entity.id, entity);
|
||||
}, new Map(store));
|
||||
store[entity.id] = entity;
|
||||
return store;
|
||||
}, { ...store });
|
||||
};
|
||||
|
||||
/** Update the list with new entity IDs. */
|
||||
|
@ -18,8 +19,8 @@ const updateList = (list: EntityList, entities: Entity[]): EntityList => {
|
|||
|
||||
/** Create an empty entity cache. */
|
||||
const createCache = (): EntityCache => ({
|
||||
store: new Map(),
|
||||
lists: new Map(),
|
||||
store: {},
|
||||
lists: {},
|
||||
});
|
||||
|
||||
/** Create an empty entity list. */
|
||||
|
|
|
@ -17,6 +17,7 @@ import ScrollableList from 'soapbox/components/scrollable-list';
|
|||
import { Column } from 'soapbox/components/ui';
|
||||
import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder-notification';
|
||||
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
|
||||
import { useNotifications } from 'soapbox/hooks/useNotifications';
|
||||
|
||||
import FilterBar from './components/filter-bar';
|
||||
import Notification from './components/notification';
|
||||
|
@ -50,24 +51,24 @@ const Notifications = () => {
|
|||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
|
||||
const {
|
||||
entities: notifications,
|
||||
isLoading,
|
||||
isFetching,
|
||||
hasNextPage: hasMore,
|
||||
fetchEntities,
|
||||
} = useNotifications();
|
||||
|
||||
const showFilterBar = settings.getIn(['notifications', 'quickFilter', 'show']);
|
||||
const activeFilter = settings.getIn(['notifications', 'quickFilter', 'active']);
|
||||
const notifications = useAppSelector(state => getNotifications(state));
|
||||
const isLoading = useAppSelector(state => state.notifications.isLoading);
|
||||
// const isUnread = useAppSelector(state => state.notifications.unread > 0);
|
||||
const hasMore = useAppSelector(state => state.notifications.hasMore);
|
||||
const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0);
|
||||
|
||||
const node = useRef<VirtuosoHandle>(null);
|
||||
const column = useRef<HTMLDivElement>(null);
|
||||
const scrollableContentRef = useRef<ImmutableList<JSX.Element> | null>(null);
|
||||
|
||||
// const handleLoadGap = (maxId) => {
|
||||
// dispatch(expandNotifications({ maxId }));
|
||||
// };
|
||||
const scrollableContentRef = useRef<Iterable<JSX.Element> | null>(null);
|
||||
|
||||
const handleLoadOlder = useCallback(debounce(() => {
|
||||
const last = notifications.last();
|
||||
const last = notifications[notifications.length];
|
||||
dispatch(expandNotifications({ maxId: last && last.get('id') }));
|
||||
}, 300, { leading: true }), [notifications]);
|
||||
|
||||
|
@ -112,6 +113,12 @@ const Notifications = () => {
|
|||
return dispatch(expandNotifications());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetching) {
|
||||
fetchEntities();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleDequeueNotifications();
|
||||
dispatch(scrollTopNotifications(true));
|
||||
|
@ -128,7 +135,7 @@ const Notifications = () => {
|
|||
? <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />
|
||||
: <FormattedMessage id='empty_column.notifications_filtered' defaultMessage="You don't have any notifications of this type yet." />;
|
||||
|
||||
let scrollableContent: ImmutableList<JSX.Element> | null = null;
|
||||
let scrollableContent: Iterable<JSX.Element> | null = null;
|
||||
|
||||
const filterBarContainer = showFilterBar
|
||||
? (<FilterBar />)
|
||||
|
@ -136,7 +143,7 @@ const Notifications = () => {
|
|||
|
||||
if (isLoading && scrollableContentRef.current) {
|
||||
scrollableContent = scrollableContentRef.current;
|
||||
} else if (notifications.size > 0 || hasMore) {
|
||||
} else if (notifications.length > 0 || hasMore) {
|
||||
scrollableContent = notifications.map((item) => (
|
||||
<Notification
|
||||
key={item.id}
|
||||
|
@ -156,7 +163,7 @@ const Notifications = () => {
|
|||
ref={node}
|
||||
scrollKey='notifications'
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && notifications.size === 0}
|
||||
showLoading={isLoading && notifications.length === 0}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={emptyMessage}
|
||||
placeholderComponent={PlaceholderNotification}
|
||||
|
@ -165,8 +172,8 @@ const Notifications = () => {
|
|||
onScrollToTop={handleScrollToTop}
|
||||
onScroll={handleScroll}
|
||||
className={classNames({
|
||||
'divide-y divide-gray-200 dark:divide-primary-800 divide-solid': notifications.size > 0,
|
||||
'space-y-2': notifications.size === 0,
|
||||
'divide-y divide-gray-200 dark:divide-primary-800 divide-solid': notifications.length > 0,
|
||||
'space-y-2': notifications.length === 0,
|
||||
})}
|
||||
>
|
||||
{scrollableContent as ImmutableList<JSX.Element>}
|
||||
|
|
18
app/soapbox/hooks/useNotifications.ts
Normal file
18
app/soapbox/hooks/useNotifications.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { normalizeNotification } from 'soapbox/normalizers';
|
||||
|
||||
import type { Notification } from 'soapbox/types/entities';
|
||||
|
||||
function useNotifications() {
|
||||
const result = useEntities<Notification>(['Notification', ''], '/api/v1/notifications');
|
||||
|
||||
return {
|
||||
...result,
|
||||
// TODO: handle this in the reducer by passing config.
|
||||
entities: result.entities.map(normalizeNotification),
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
useNotifications,
|
||||
};
|
Loading…
Reference in a new issue