diff --git a/app/soapbox/entity-store/hooks/useEntities.ts b/app/soapbox/entity-store/hooks/useEntities.ts index 10abc0d4da..e525a86a77 100644 --- a/app/soapbox/entity-store/hooks/useEntities.ts +++ b/app/soapbox/entity-store/hooks/useEntities.ts @@ -13,14 +13,14 @@ function useEntities(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((result, id) => { - const entity = cache?.store.get(id) as TEntity | undefined; + const entity = cache?.store[id] as TEntity | undefined; if (entity) { result.push(entity); } diff --git a/app/soapbox/entity-store/hooks/useEntity.ts b/app/soapbox/entity-store/hooks/useEntity.ts index 2f37df2f38..2861a25331 100644 --- a/app/soapbox/entity-store/hooks/useEntity.ts +++ b/app/soapbox/entity-store/hooks/useEntity.ts @@ -13,7 +13,7 @@ function useEntity(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; diff --git a/app/soapbox/entity-store/reducer.ts b/app/soapbox/entity-store/reducer.ts index 90170d9449..f2af680a18 100644 --- a/app/soapbox/entity-store/reducer.ts +++ b/app/soapbox/entity-store/reducer.ts @@ -14,30 +14,32 @@ import type { Entity, EntityCache, EntityListState } from './types'; enableMapSet(); /** Entity reducer state. */ -type State = Map; +interface State { + [entityType: string]: EntityCache | undefined +} /** Import entities into the cache. */ const importEntities = ( - state: Readonly, + 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 = new Map(), action: EntityAction): State { +function reducer(state: Readonly = {}, action: EntityAction): State { switch (action.type) { case ENTITIES_IMPORT: return importEntities(state, action.entityType, action.entities, action.listKey); diff --git a/app/soapbox/entity-store/types.ts b/app/soapbox/entity-store/types.ts index d114960b74..4e71201200 100644 --- a/app/soapbox/entity-store/types.ts +++ b/app/soapbox/entity-store/types.ts @@ -5,7 +5,9 @@ interface Entity { } /** Store of entities by ID. */ -type EntityStore = Map +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 + lists: { + [listKey: string]: EntityList | undefined + } } export { diff --git a/app/soapbox/entity-store/utils.ts b/app/soapbox/entity-store/utils.ts index 445cb44e9c..fcff11a3ec 100644 --- a/app/soapbox/entity-store/utils.ts +++ b/app/soapbox/entity-store/utils.ts @@ -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((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. */ diff --git a/app/soapbox/features/notifications/index.tsx b/app/soapbox/features/notifications/index.tsx index 820bb64c33..a9648e7eb6 100644 --- a/app/soapbox/features/notifications/index.tsx +++ b/app/soapbox/features/notifications/index.tsx @@ -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(null); const column = useRef(null); - const scrollableContentRef = useRef | null>(null); - - // const handleLoadGap = (maxId) => { - // dispatch(expandNotifications({ maxId })); - // }; + const scrollableContentRef = useRef | 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 = () => { ? : ; - let scrollableContent: ImmutableList | null = null; + let scrollableContent: Iterable | null = null; const filterBarContainer = showFilterBar ? () @@ -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) => ( { 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} diff --git a/app/soapbox/hooks/useNotifications.ts b/app/soapbox/hooks/useNotifications.ts new file mode 100644 index 0000000000..96181c4573 --- /dev/null +++ b/app/soapbox/hooks/useNotifications.ts @@ -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', ''], '/api/v1/notifications'); + + return { + ...result, + // TODO: handle this in the reducer by passing config. + entities: result.entities.map(normalizeNotification), + }; +} + +export { + useNotifications, +}; \ No newline at end of file