EntityStore: incorporate Notifications, go back to using POJOs instead of Maps

This commit is contained in:
Alex Gleason 2022-12-04 18:54:54 -06:00
parent f7bfc40b70
commit 27500193d8
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
7 changed files with 70 additions and 38 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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);

View file

@ -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 {

View file

@ -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. */

View file

@ -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>}

View 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,
};