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

entity-store
Alex Gleason 2022-12-04 18:54:54 -06:00
rodzic f7bfc40b70
commit 27500193d8
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
7 zmienionych plików z 70 dodań i 38 usunięć

Wyświetl plik

@ -13,14 +13,14 @@ function useEntities<TEntity extends Entity>(path: EntityPath, endpoint: string)
const [entityType, listKey] = path; const [entityType, listKey] = path;
const cache = useAppSelector(state => state.entities.get(entityType)); const cache = useAppSelector(state => state.entities[entityType]);
const list = cache?.lists.get(listKey); const list = cache?.lists[listKey];
const entityIds = list?.ids; const entityIds = list?.ids;
const entities: readonly TEntity[] = entityIds ? ( const entities: readonly TEntity[] = entityIds ? (
Array.from(entityIds).reduce<TEntity[]>((result, id) => { 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) { if (entity) {
result.push(entity); result.push(entity);
} }

Wyświetl plik

@ -13,7 +13,7 @@ function useEntity<TEntity extends Entity>(path: EntityPath, endpoint: string) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [entityType, entityId] = path; 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 [isFetching, setIsFetching] = useState(false);
const isLoading = isFetching && !entity; const isLoading = isFetching && !entity;

Wyświetl plik

@ -14,30 +14,32 @@ import type { Entity, EntityCache, EntityListState } from './types';
enableMapSet(); enableMapSet();
/** Entity reducer state. */ /** Entity reducer state. */
type State = Map<string, EntityCache>; interface State {
[entityType: string]: EntityCache | undefined
}
/** Import entities into the cache. */ /** Import entities into the cache. */
const importEntities = ( const importEntities = (
state: Readonly<State>, state: State,
entityType: string, entityType: string,
entities: Entity[], entities: Entity[],
listKey?: string, listKey?: string,
newState?: EntityListState, newState?: EntityListState,
): State => { ): State => {
return produce(state, draft => { return produce(state, draft => {
const cache = draft.get(entityType) ?? createCache(); const cache = draft[entityType] ?? createCache();
cache.store = updateStore(cache.store, entities); cache.store = updateStore(cache.store, entities);
if (listKey) { if (typeof listKey === 'string') {
let list = cache.lists.get(listKey) ?? createList(); let list = { ...(cache.lists[listKey] ?? createList()) };
list = updateList(list, entities); list = updateList(list, entities);
if (newState) { if (newState) {
list.state = 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, isFetching: boolean,
) => { ) => {
return produce(state, draft => { return produce(state, draft => {
const cache = draft.get(entityType) ?? createCache(); const cache = draft[entityType] ?? createCache();
if (listKey) { if (typeof listKey === 'string') {
const list = cache.lists.get(listKey) ?? createList(); const list = cache.lists[listKey] ?? createList();
list.state.fetching = isFetching; 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. */ /** 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) { switch (action.type) {
case ENTITIES_IMPORT: case ENTITIES_IMPORT:
return importEntities(state, action.entityType, action.entities, action.listKey); return importEntities(state, action.entityType, action.entities, action.listKey);

Wyświetl plik

@ -5,7 +5,9 @@ interface Entity {
} }
/** Store of entities by ID. */ /** Store of entities by ID. */
type EntityStore = Map<string, Entity> interface EntityStore {
[id: string]: Entity | undefined
}
/** List of entity IDs and fetch state. */ /** List of entity IDs and fetch state. */
interface EntityList { interface EntityList {
@ -32,7 +34,9 @@ interface EntityCache {
/** Map of entities of this type. */ /** Map of entities of this type. */
store: EntityStore store: EntityStore
/** Lists of entity IDs for a particular purpose. */ /** Lists of entity IDs for a particular purpose. */
lists: Map<string, EntityList> lists: {
[listKey: string]: EntityList | undefined
}
} }
export { export {

Wyświetl plik

@ -3,8 +3,9 @@ import type { Entity, EntityStore, EntityList, EntityCache } from './types';
/** Insert the entities into the store. */ /** Insert the entities into the store. */
const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => { const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => {
return entities.reduce<EntityStore>((store, entity) => { return entities.reduce<EntityStore>((store, entity) => {
return store.set(entity.id, entity); store[entity.id] = entity;
}, new Map(store)); return store;
}, { ...store });
}; };
/** Update the list with new entity IDs. */ /** Update the list with new entity IDs. */
@ -18,8 +19,8 @@ const updateList = (list: EntityList, entities: Entity[]): EntityList => {
/** Create an empty entity cache. */ /** Create an empty entity cache. */
const createCache = (): EntityCache => ({ const createCache = (): EntityCache => ({
store: new Map(), store: {},
lists: new Map(), lists: {},
}); });
/** Create an empty entity list. */ /** Create an empty entity list. */

Wyświetl plik

@ -17,6 +17,7 @@ import ScrollableList from 'soapbox/components/scrollable-list';
import { Column } from 'soapbox/components/ui'; import { Column } from 'soapbox/components/ui';
import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder-notification'; import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder-notification';
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
import { useNotifications } from 'soapbox/hooks/useNotifications';
import FilterBar from './components/filter-bar'; import FilterBar from './components/filter-bar';
import Notification from './components/notification'; import Notification from './components/notification';
@ -50,24 +51,24 @@ const Notifications = () => {
const intl = useIntl(); const intl = useIntl();
const settings = useSettings(); const settings = useSettings();
const {
entities: notifications,
isLoading,
isFetching,
hasNextPage: hasMore,
fetchEntities,
} = useNotifications();
const showFilterBar = settings.getIn(['notifications', 'quickFilter', 'show']); const showFilterBar = settings.getIn(['notifications', 'quickFilter', 'show']);
const activeFilter = settings.getIn(['notifications', 'quickFilter', 'active']); 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 totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0);
const node = useRef<VirtuosoHandle>(null); const node = useRef<VirtuosoHandle>(null);
const column = useRef<HTMLDivElement>(null); const column = useRef<HTMLDivElement>(null);
const scrollableContentRef = useRef<ImmutableList<JSX.Element> | null>(null); const scrollableContentRef = useRef<Iterable<JSX.Element> | null>(null);
// const handleLoadGap = (maxId) => {
// dispatch(expandNotifications({ maxId }));
// };
const handleLoadOlder = useCallback(debounce(() => { const handleLoadOlder = useCallback(debounce(() => {
const last = notifications.last(); const last = notifications[notifications.length];
dispatch(expandNotifications({ maxId: last && last.get('id') })); dispatch(expandNotifications({ maxId: last && last.get('id') }));
}, 300, { leading: true }), [notifications]); }, 300, { leading: true }), [notifications]);
@ -112,6 +113,12 @@ const Notifications = () => {
return dispatch(expandNotifications()); return dispatch(expandNotifications());
}; };
useEffect(() => {
if (!isFetching) {
fetchEntities();
}
}, []);
useEffect(() => { useEffect(() => {
handleDequeueNotifications(); handleDequeueNotifications();
dispatch(scrollTopNotifications(true)); 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' 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." />; : <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 const filterBarContainer = showFilterBar
? (<FilterBar />) ? (<FilterBar />)
@ -136,7 +143,7 @@ const Notifications = () => {
if (isLoading && scrollableContentRef.current) { if (isLoading && scrollableContentRef.current) {
scrollableContent = scrollableContentRef.current; scrollableContent = scrollableContentRef.current;
} else if (notifications.size > 0 || hasMore) { } else if (notifications.length > 0 || hasMore) {
scrollableContent = notifications.map((item) => ( scrollableContent = notifications.map((item) => (
<Notification <Notification
key={item.id} key={item.id}
@ -156,7 +163,7 @@ const Notifications = () => {
ref={node} ref={node}
scrollKey='notifications' scrollKey='notifications'
isLoading={isLoading} isLoading={isLoading}
showLoading={isLoading && notifications.size === 0} showLoading={isLoading && notifications.length === 0}
hasMore={hasMore} hasMore={hasMore}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
placeholderComponent={PlaceholderNotification} placeholderComponent={PlaceholderNotification}
@ -165,8 +172,8 @@ const Notifications = () => {
onScrollToTop={handleScrollToTop} onScrollToTop={handleScrollToTop}
onScroll={handleScroll} onScroll={handleScroll}
className={classNames({ className={classNames({
'divide-y divide-gray-200 dark:divide-primary-800 divide-solid': notifications.size > 0, 'divide-y divide-gray-200 dark:divide-primary-800 divide-solid': notifications.length > 0,
'space-y-2': notifications.size === 0, 'space-y-2': notifications.length === 0,
})} })}
> >
{scrollableContent as ImmutableList<JSX.Element>} {scrollableContent as ImmutableList<JSX.Element>}

Wyświetl plik

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