sforkowany z mirror/soapbox
EntityStore: incorporate Notifications, go back to using POJOs instead of Maps
rodzic
f7bfc40b70
commit
27500193d8
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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>}
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
Ładowanie…
Reference in New Issue