// // Copyright © 2023 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // import activity, { type ActivityMessage } from '@hcengineering/activity' import { SortingOrder, getCurrentAccount, type Class, type Doc, type Ref, type TxOperations, type WithLookup, generateId } from '@hcengineering/core' import notification, { type ActivityInboxNotification, type Collaborators, type DocNotifyContext, type InboxNotification, type InboxNotificationsClient } from '@hcengineering/notification' import { createQuery, getClient } from '@hcengineering/presentation' import { derived, get, writable } from 'svelte/store' export const inboxMessagesStore = writable([]) /** * @public */ export class InboxNotificationsClientImpl implements InboxNotificationsClient { protected static _instance: InboxNotificationsClientImpl | undefined = undefined readonly docNotifyContexts = writable([]) readonly docNotifyContextByDoc = writable, DocNotifyContext>>(new Map()) readonly activityInboxNotifications = writable>>([]) readonly otherInboxNotifications = writable([]) readonly inboxNotifications = derived( [this.activityInboxNotifications, this.otherInboxNotifications], ([activityNotifications, otherNotifications]) => { return otherNotifications .concat(activityNotifications) .sort((n1, n2) => (n2.createdOn ?? n2.modifiedOn) - (n1.createdOn ?? n1.modifiedOn)) }, [] as InboxNotification[] ) readonly inboxNotificationsByContext = derived( [this.docNotifyContexts, this.inboxNotifications], ([notifyContexts, inboxNotifications]) => { if (inboxNotifications.length === 0 || notifyContexts.length === 0) { return new Map, InboxNotification[]>() } return inboxNotifications.reduce((result, notification) => { const notifyContext = notifyContexts.find(({ _id }) => _id === notification.docNotifyContext) if (notifyContext === undefined || notifyContext.hidden) { return result } return result.set(notifyContext._id, (result.get(notifyContext._id) ?? []).concat(notification)) }, new Map, InboxNotification[]>()) } ) private readonly docNotifyContextsQuery = createQuery(true) private readonly otherInboxNotificationsQuery = createQuery(true) private readonly activityInboxNotificationsQuery = createQuery(true) private _docNotifyContextByDoc = new Map, DocNotifyContext>() private constructor () { this.docNotifyContextsQuery.query( notification.class.DocNotifyContext, { user: getCurrentAccount()._id }, (result: DocNotifyContext[]) => { this.docNotifyContexts.set(result) this._docNotifyContextByDoc = new Map(result.map((updates) => [updates.attachedTo, updates])) this.docNotifyContextByDoc.set(this._docNotifyContextByDoc) } ) this.otherInboxNotificationsQuery.query( notification.class.InboxNotification, { _class: { $nin: [notification.class.ActivityInboxNotification] }, user: getCurrentAccount()._id }, (result: InboxNotification[]) => { this.otherInboxNotifications.set(result) }, { sort: { createdOn: SortingOrder.Descending } } ) this.activityInboxNotificationsQuery.query( notification.class.ActivityInboxNotification, { user: getCurrentAccount()._id }, (result: ActivityInboxNotification[]) => { this.activityInboxNotifications.set(result) }, { sort: { createdOn: SortingOrder.Descending }, lookup: { attachedTo: activity.class.ActivityMessage }, limit: 1000 } ) } static createClient (): InboxNotificationsClientImpl { InboxNotificationsClientImpl._instance = new InboxNotificationsClientImpl() return InboxNotificationsClientImpl._instance } static getClient (): InboxNotificationsClientImpl { if (InboxNotificationsClientImpl._instance === undefined) { InboxNotificationsClientImpl._instance = new InboxNotificationsClientImpl() } return InboxNotificationsClientImpl._instance } async readDoc (client: TxOperations, _id: Ref): Promise { const docNotifyContext = this._docNotifyContextByDoc.get(_id) if (docNotifyContext === undefined) { return } const inboxNotifications = (get(this.inboxNotifications) ?? []).filter( (notification) => notification.docNotifyContext === docNotifyContext._id && !notification.isViewed ) for (const notification of inboxNotifications) { await client.update(notification, { isViewed: true }) } await client.update(docNotifyContext, { lastViewedTimestamp: Date.now() }) } async forceReadDoc (client: TxOperations, _id: Ref, _class: Ref>): Promise { const context = this._docNotifyContextByDoc.get(_id) if (context !== undefined) { await this.readDoc(client, _id) return } const doc = await client.findOne(_class, { _id }) if (doc === undefined) { return } const hierarchy = client.getHierarchy() const collaboratorsMixin = hierarchy.as(doc, notification.mixin.Collaborators) if (collaboratorsMixin.collaborators === undefined) { await client.createMixin( collaboratorsMixin._id, collaboratorsMixin._class, collaboratorsMixin.space, notification.mixin.Collaborators, { collaborators: [getCurrentAccount()._id] } ) } else if (!collaboratorsMixin.collaborators.includes(getCurrentAccount()._id)) { await client.updateMixin( collaboratorsMixin._id, collaboratorsMixin._class, collaboratorsMixin.space, notification.mixin.Collaborators, { $push: { collaborators: getCurrentAccount()._id } } ) } await client.createDoc(notification.class.DocNotifyContext, doc.space, { attachedTo: _id, attachedToClass: _class, user: getCurrentAccount()._id, hidden: true }) } async readMessages (client: TxOperations, ids: Array>): Promise { const alreadyReadIds = get(this.activityInboxNotifications) .filter(({ attachedTo, isViewed }) => ids.includes(attachedTo) && isViewed) .map(({ attachedTo }) => attachedTo) const toReadIds = ids.filter((id) => !alreadyReadIds.includes(id)) if (toReadIds.length === 0) { return } const notificationsToRead = await client.findAll(notification.class.ActivityInboxNotification, { user: getCurrentAccount()._id, attachedTo: { $in: toReadIds }, isViewed: { $ne: true } }) for (const notification of notificationsToRead) { await client.update(notification, { isViewed: true }) } } async readNotifications (client: TxOperations, ids: Array>): Promise { const notificationsToRead = (get(this.inboxNotifications) ?? []).filter(({ _id }) => ids.includes(_id)) for (const notification of notificationsToRead) { await client.update(notification, { isViewed: true }) } } async unreadNotifications (client: TxOperations, ids: Array>): Promise { const notificationsToUnread = (get(this.inboxNotifications) ?? []).filter(({ _id }) => ids.includes(_id)) for (const notification of notificationsToUnread) { await client.update(notification, { isViewed: false }) } } async deleteNotifications (client: TxOperations, ids: Array>): Promise { const inboxNotifications = (get(this.inboxNotifications) ?? []).filter(({ _id }) => ids.includes(_id)) for (const notification of inboxNotifications) { await client.remove(notification) } } async deleteAllNotifications (): Promise { const doneOp = await getClient().measure('deleteAllNotifications') const ops = getClient().apply(generateId()) try { const inboxNotifications = get(this.inboxNotifications) ?? [] const contexts = get(this.docNotifyContexts) ?? [] for (const notification of inboxNotifications) { await ops.remove(notification) } for (const context of contexts) { await ops.update(context, { lastViewedTimestamp: Date.now() }) } } finally { await ops.commit() await doneOp() } } async readAllNotifications (): Promise { const doneOp = await getClient().measure('readAllNotifications') const ops = getClient().apply(generateId()) try { const inboxNotifications = get(this.inboxNotifications) ?? [] const contexts = get(this.docNotifyContexts) ?? [] for (const notification of inboxNotifications) { if (!notification.isViewed) { await ops.update(notification, { isViewed: true }) } } for (const context of contexts) { await ops.update(context, { lastViewedTimestamp: Date.now() }) } } finally { await ops.commit() await doneOp() } } async unreadAllNotifications (): Promise { const doneOp = await getClient().measure('unreadAllNotifications') const ops = getClient().apply(generateId()) try { const inboxNotifications = get(this.inboxNotifications) ?? [] const contexts = get(this.docNotifyContexts) ?? [] for (const notification of inboxNotifications) { if (notification.isViewed) { await ops.update(notification, { isViewed: false }) } } for (const context of contexts) { await ops.update(context, { lastViewedTimestamp: 0 }) } } finally { await ops.commit() await doneOp() } } }