@magicbell/react-headless
Version:
Hooks to build a notification inbox
381 lines • 19.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const immer_1 = require("immer");
const ramda_1 = require("ramda");
const zustand_1 = require("zustand");
const realtime_js_1 = require("../../lib/realtime.js");
const buildStore_js_1 = tslib_1.__importDefault(require("./helpers/buildStore.js"));
const setStoreProps_js_1 = tslib_1.__importDefault(require("./helpers/setStoreProps.js"));
const strategies_js_1 = require("./helpers/strategies.js");
const NotificationRepository_js_1 = tslib_1.__importDefault(require("./NotificationRepository.js"));
function unix() {
return Math.floor(Date.now() / 1000);
}
function addToStore(store, notification) {
store.total += 1;
const idx = store.notifications.findIndex((n) => n.sentAt < notification.sentAt);
if (idx === -1) {
store.notifications.push(notification);
}
else {
store.notifications.splice(idx, 0, notification);
}
}
/**
* Collection of notifications store. It contains all stores of notifications
* and exposes methods to interact with them.
*
* @private Use the `useNotifications` hook instead.
*/
const useNotificationStoresCollection = (0, zustand_1.create)((set, get) => ({
stores: {},
_repository: new NotificationRepository_js_1.default(),
setStore: (storeId, defaultQueryParams = {}, otherProps = {}) => {
set((0, immer_1.produce)((draft) => {
draft.stores[storeId] = (0, buildStore_js_1.default)({ ...otherProps, context: defaultQueryParams });
}));
},
fetchStore: async (storeId, queryParams = {}, options = {}) => {
const { stores, _repository } = get();
const store = stores[storeId];
if (store) {
const response = await _repository.findBy({ ...store.context, ...queryParams });
if (!response)
return;
set((0, immer_1.produce)((draft) => {
draft.stores[storeId] = (0, setStoreProps_js_1.default)(store, { ...response, lastFetchedAt: new Date() }, options);
}));
}
else {
// Provided for development support.
// eslint-disable-next-line no-console
console.error(`Store not found. Define a store with the ${storeId} ID`);
}
},
fetchAllStores: async (queryParams = {}, options = {}) => {
const { stores, fetchStore } = get();
const storeIds = Object.keys(stores);
const fetchers = storeIds.map((storeId) => fetchStore(storeId, queryParams, options));
await Promise.all(fetchers);
},
markNotificationAsSeen: (notification) => {
const { stores } = get();
const notificationId = notification.id;
(0, realtime_js_1.emitEvent)('notifications.seen', notification, 'local');
set((0, immer_1.produce)((draft) => {
for (const storeId in stores) {
const { notifications, unseenCount } = stores[storeId];
const index = (0, ramda_1.findIndex)((0, ramda_1.propEq)('id', notificationId), notifications);
if (index > -1) {
const notification = notifications[index];
if (!notification.seenAt) {
draft.stores[storeId].unseenCount = Math.max(0, unseenCount - 1);
draft.stores[storeId].notifications[index] = (0, ramda_1.mergeRight)(notifications[index], {
seenAt: unix(),
});
}
}
}
}));
},
markNotificationAsRead: (notification) => {
const { stores, _repository } = get();
const { id: notificationId } = notification;
const promise = _repository.markAsRead(notificationId);
(0, realtime_js_1.emitEvent)('notifications.read', notification, 'local');
set((0, immer_1.produce)((draft) => {
const now = unix();
const attrs = { readAt: now, seenAt: now };
for (const storeId in stores) {
const { total, notifications, context, unreadCount, unseenCount } = stores[storeId];
const index = (0, ramda_1.findIndex)((0, ramda_1.propEq)('id', notificationId), notifications);
if (index > -1) {
// Decrease the counters if the notification wasn't already read|seen, and clamp to zero as it might happen
// that markAsSeen has set the counter to zero, while the notification.seenAt is still undefined
if (!notification.readAt)
draft.stores[storeId].unreadCount = Math.max(0, unreadCount - 1);
if (!notification.seenAt)
draft.stores[storeId].unseenCount = Math.max(0, unseenCount - 1);
const readNotification = (0, ramda_1.mergeRight)(notifications[index], attrs);
if ((0, strategies_js_1.objMatchesContext)(readNotification, context).result) {
// Update the store
draft.stores[storeId].notifications[index] = readNotification;
}
else {
// Remove notification from the store
draft.stores[storeId].total = Math.max(0, total - 1);
draft.stores[storeId].notifications.splice(index, 1);
}
}
else {
const readNotification = (0, ramda_1.mergeRight)(notification, attrs);
if ((0, strategies_js_1.objMatchesContext)(readNotification, context).result) {
addToStore(draft.stores[storeId], readNotification);
}
}
}
}));
return promise;
},
markNotificationAsUnread: (notification) => {
const { stores, _repository } = get();
const { id: notificationId } = notification;
const promise = _repository.markAsUnread(notificationId);
(0, realtime_js_1.emitEvent)('notifications.unread', notification, 'local');
set((0, immer_1.produce)((draft) => {
const attrs = { readAt: null };
for (const storeId in stores) {
const { notifications, context } = stores[storeId];
const index = (0, ramda_1.findIndex)((0, ramda_1.propEq)('id', notificationId), notifications);
if (index > -1) {
const unreadNotification = (0, ramda_1.mergeRight)(notifications[index], attrs);
if ((0, strategies_js_1.objMatchesContext)(unreadNotification, context).result) {
// Update the store
if (notification.readAt)
draft.stores[storeId].unreadCount += 1;
draft.stores[storeId].notifications[index] = unreadNotification;
}
else {
// Remove notification from the store
draft.stores[storeId].total = Math.max(0, draft.stores[storeId].total - 1);
draft.stores[storeId].notifications.splice(index, 1);
}
}
else {
const unreadNotification = (0, ramda_1.mergeRight)(notification, attrs);
if ((0, strategies_js_1.objMatchesContext)(unreadNotification, context).result) {
addToStore(draft.stores[storeId], unreadNotification);
if (notification.readAt)
draft.stores[storeId].unreadCount += 1;
}
}
}
}));
return promise;
},
archiveNotification: (notification, options = {}) => {
const { stores, _repository } = get();
const { id: notificationId } = notification;
let promise = Promise.resolve(true);
// Do not persist the state if this op is a consequence of a remote event.
if (options.persist !== false) {
promise = _repository.archive(notificationId);
(0, realtime_js_1.emitEvent)('notifications.archived', notification, 'local');
}
set((0, immer_1.produce)((draft) => {
const now = unix();
const attrs = { archivedAt: now };
for (const storeId in stores) {
const store = stores[storeId];
const index = (0, ramda_1.findIndex)((0, ramda_1.propEq)('id', notificationId), store.notifications);
if (index > -1) {
const newNotification = { ...store.notifications[index], ...attrs };
if ((0, strategies_js_1.objMatchesContext)(newNotification, store.context).result) {
// Update the store
draft.stores[storeId].notifications[index] = newNotification;
}
else {
// Decrease the counters if the notification was included in it, and clamp to zero
if (!notification.readAt)
draft.stores[storeId].unreadCount = Math.max(0, store.unreadCount - 1);
if (!notification.seenAt)
draft.stores[storeId].unseenCount = Math.max(0, store.unseenCount - 1);
// Remove notification from the store
draft.stores[storeId].total = Math.max(0, store.total - 1);
draft.stores[storeId].notifications.splice(index, 1);
}
}
else {
const newNotification = { ...notification, ...attrs };
if ((0, strategies_js_1.objMatchesContext)(newNotification, store.context).result) {
// Add the notification to the store
if (!notification.readAt)
draft.stores[storeId].unreadCount += 1;
if (!notification.seenAt)
draft.stores[storeId].unseenCount += 1;
addToStore(draft.stores[storeId], newNotification);
}
}
}
}));
return promise;
},
unarchiveNotification: (notification, options = {}) => {
const { stores, _repository } = get();
const { id: notificationId } = notification;
let promise = Promise.resolve(true);
// Do not persist the state if this op is a consequence of a remote event.
if (options.persist !== false) {
promise = _repository.unarchive(notificationId);
(0, realtime_js_1.emitEvent)('notifications.unarchived', notification, 'local');
}
set((0, immer_1.produce)((draft) => {
const attrs = { archivedAt: null };
for (const storeId in stores) {
const store = stores[storeId];
const index = (0, ramda_1.findIndex)((0, ramda_1.propEq)('id', notificationId), store.notifications);
if (index > -1) {
const newNotification = { ...store.notifications[index], ...attrs };
if ((0, strategies_js_1.objMatchesContext)(newNotification, store.context).result) {
draft.stores[storeId].notifications[index] = newNotification;
}
else {
// Remove notification from the store
if (!notification.readAt)
draft.stores[storeId].unreadCount = Math.max(0, store.unreadCount - 1);
if (!notification.seenAt)
draft.stores[storeId].unseenCount = Math.max(0, store.unseenCount - 1);
draft.stores[storeId].total = Math.max(0, store.total - 1);
draft.stores[storeId].notifications.splice(index, 1);
}
}
else {
const newNotification = { ...notification, ...attrs };
if ((0, strategies_js_1.objMatchesContext)(newNotification, store.context).result) {
// Add the notification to the store
if (!notification.readAt)
draft.stores[storeId].unreadCount += 1;
if (!notification.seenAt)
draft.stores[storeId].unseenCount += 1;
addToStore(draft.stores[storeId], newNotification);
}
}
}
}));
return promise;
},
deleteNotification: (notification, options = {}) => {
const { stores, _repository } = get();
const notificationId = notification.id;
let promise = Promise.resolve(true);
// Do not persist the state is this op is a consequence of a remote event.
// Neither emit a local event.
if (options.persist !== false) {
promise = _repository.delete(notificationId);
(0, realtime_js_1.emitEvent)('notifications.delete', notification, 'local');
}
set((0, immer_1.produce)((draft) => {
for (const storeId in stores) {
const { notifications, total, unseenCount, unreadCount } = stores[storeId];
const index = (0, ramda_1.findIndex)((0, ramda_1.propEq)('id', notificationId), notifications);
if (index > -1) {
const notification = notifications[index];
if (!notification.seenAt)
draft.stores[storeId].unseenCount = Math.max(0, unseenCount - 1);
if (!notification.readAt)
draft.stores[storeId].unreadCount = Math.max(0, unreadCount - 1);
draft.stores[storeId].total = Math.max(0, total - 1);
draft.stores[storeId].notifications.splice(index, 1);
}
}
}));
return promise;
},
markAllAsSeen: (options = { persist: true, updateModels: true }) => {
const { stores, _repository } = get();
let promise = Promise.resolve(true);
// Do not persist the state if this op is a consequence of a remote event.
// Neither emit a local event.
if (options.persist !== false) {
const params = options.storeId ? stores[options.storeId]?.context : {};
promise = _repository.markAllAsSeen(params);
(0, realtime_js_1.emitEvent)('notifications.seen.all', null, 'local');
}
set((0, immer_1.produce)((draft) => {
const changedNotifications = new Map();
const now = unix();
for (const storeId in stores) {
const { context } = stores[storeId];
draft.stores[storeId].unseenCount = 0;
if (options.updateModels === false)
continue;
// don't iterate over notifications in stores that only hold seen notifications
if (context.seen !== true) {
for (const notification of draft.stores[storeId].notifications) {
// don't change notifications that are already seen
if (notification.seenAt)
continue;
notification.seenAt = now;
changedNotifications.set(notification.id, notification);
}
}
// stores that don't include seen notifications, can be flushed
if (context.seen === false) {
draft.stores[storeId].notifications = [];
draft.stores[storeId].total = 0;
}
}
// do a second pass to update the notifications in the stores that don't hold seen notifications
for (const storeId in stores) {
const { context } = stores[storeId];
// skip stores that already hold seen notifications
if (context.seen !== true)
continue;
const notifications = draft.stores[storeId].notifications;
for (const notification of changedNotifications.values()) {
if ((0, strategies_js_1.objMatchesContext)(notification, context).result &&
!notifications.find((n) => n.id === notification.id)) {
addToStore(draft.stores[storeId], notification);
}
}
}
}));
return promise;
},
markAllAsRead: (options = { persist: true, updateModels: true }) => {
const { stores, _repository } = get();
let promise = Promise.resolve(true);
// Do not persist the state if this op is a consequence of a remote event.
// Neither emit a local event.
if (options.persist !== false) {
const params = options.storeId ? stores[options.storeId]?.context : {};
promise = _repository.markAllAsRead(params);
(0, realtime_js_1.emitEvent)('notifications.read.all', null, 'local');
}
set((0, immer_1.produce)((draft) => {
const changedNotifications = new Map();
const now = unix();
for (const storeId in stores) {
const { context } = stores[storeId];
draft.stores[storeId].unreadCount = 0;
draft.stores[storeId].unseenCount = 0;
if (options.updateModels === false)
continue;
// don't iterate over notifications in stores that only hold read notifications
if (context.read !== true) {
for (const notification of draft.stores[storeId].notifications) {
// don't change notifications that are already read
if (notification.readAt)
continue;
notification.readAt = now;
notification.seenAt = now;
changedNotifications.set(notification.id, notification);
}
}
// stores that don't include read notifications, can be flushed
if (context.read === false) {
draft.stores[storeId].notifications = [];
draft.stores[storeId].total = 0;
}
}
// do a second run to add changed notifications to stores that didn't hold unread notifications
for (const storeId in stores) {
const { context } = stores[storeId];
// skip stores that already contain the notification
if (context.read !== true)
continue;
const notifications = draft.stores[storeId].notifications;
for (const notification of changedNotifications.values()) {
if ((0, strategies_js_1.objMatchesContext)(notification, context).result &&
!notifications.find((n) => n.id === notification.id)) {
addToStore(draft.stores[storeId], notification);
}
}
}
}));
return promise;
},
}));
exports.default = useNotificationStoresCollection;
//# sourceMappingURL=useNotificationStoresCollection.js.map