From bba3b48bb54f562b7f8f105b4a499a189e396f77 Mon Sep 17 00:00:00 2001 From: Kristina Date: Wed, 22 Jan 2025 20:45:03 +0400 Subject: [PATCH] Extract trigger for pushes (#7767) Signed-off-by: Kristina Fefelova --- models/notification/src/index.ts | 3 + models/notification/src/migration.ts | 41 +++ models/server-notification/src/index.ts | 9 + plugins/notification/src/index.ts | 2 + .../activity-resources/src/references.ts | 8 +- .../notification-resources/src/index.ts | 253 ++------------- .../notification-resources/src/push.ts | 295 ++++++++++++++++++ .../notification-resources/src/types.ts | 10 +- .../notification-resources/src/utils.ts | 8 + server-plugins/notification/src/index.ts | 3 +- server-plugins/request-resources/src/index.ts | 8 +- .../github/pod-github/src/notifications.ts | 2 + 12 files changed, 408 insertions(+), 234 deletions(-) create mode 100644 server-plugins/notification-resources/src/push.ts diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 3c2116987c1..a4199c3ee28 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -239,6 +239,9 @@ export class TInboxNotification extends TDoc implements InboxNotification { @Prop(TypeBoolean(), core.string.Boolean) archived!: boolean + objectId!: Ref + objectClass!: Ref> + declare space: Ref title?: IntlString diff --git a/models/notification/src/migration.ts b/models/notification/src/migration.ts index 972d3f77a2f..7b97e501c62 100644 --- a/models/notification/src/migration.ts +++ b/models/notification/src/migration.ts @@ -247,6 +247,43 @@ export async function migrateSettings (client: MigrationClient): Promise { ) } +export async function migrateNotificationsObject (client: MigrationClient): Promise { + while (true) { + const notifications = await client.find( + DOMAIN_NOTIFICATION, + { objectId: { $exists: false }, docNotifyContext: { $exists: true } }, + { limit: 500 } + ) + + if (notifications.length === 0) return + + const contextIds = Array.from(new Set(notifications.map((n) => n.docNotifyContext))) + const contexts = await client.find(DOMAIN_DOC_NOTIFY, { _id: { $in: contextIds } }) + + for (const context of contexts) { + await client.update( + DOMAIN_NOTIFICATION, + { docNotifyContext: context._id, objectId: { $exists: false } }, + { objectId: context.objectId, objectClass: context.objectClass } + ) + } + + const toDelete: Ref[] = [] + + for (const notification of notifications) { + const context = contexts.find((c) => c._id === notification.docNotifyContext) + + if (context === undefined) { + toDelete.push(notification._id) + } + } + + if (toDelete.length > 0) { + await client.deleteMany(DOMAIN_NOTIFICATION, { _id: { $in: toDelete } }) + } + } +} + export const notificationOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await tryMigrate(client, notificationId, [ @@ -429,6 +466,10 @@ export const notificationOperation: MigrateOperation = { func: async (client) => { await client.update(DOMAIN_DOC_NOTIFY, { space: core.space.Space }, { space: core.space.Workspace }) } + }, + { + state: 'migrate-notifications-object', + func: migrateNotificationsObject } ]) }, diff --git a/models/server-notification/src/index.ts b/models/server-notification/src/index.ts index 02d29ebba85..2e533f4608e 100644 --- a/models/server-notification/src/index.ts +++ b/models/server-notification/src/index.ts @@ -103,4 +103,13 @@ export function createModel (builder: Builder): void { mixin: contact.mixin.Employee } }) + + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverNotification.trigger.PushNotificationsHandler, + isAsync: true, + txMatch: { + _class: core.class.TxCreateDoc, + objectClass: notification.class.InboxNotification + } + }) } diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index f4a8dd2f5ee..905988109b2 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -235,6 +235,8 @@ export interface InboxNotification extends Doc { isViewed: boolean docNotifyContext: Ref + objectId: Ref + objectClass: Ref> // For browser notifications title?: IntlString diff --git a/server-plugins/activity-resources/src/references.ts b/server-plugins/activity-resources/src/references.ts index 1021de0469b..ad7db3c5a3b 100644 --- a/server-plugins/activity-resources/src/references.ts +++ b/server-plugins/activity-resources/src/references.ts @@ -151,6 +151,8 @@ export async function getPersonNotificationTxes ( messageHtml: reference.message, mentionedIn: reference.attachedDocId ?? reference.srcDocId, mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass, + objectId: reference.srcDocId, + objectClass: reference.srcDocClass, user: receiver[0]._id, isViewed: false, archived: false @@ -238,9 +240,6 @@ export async function getPersonNotificationTxes ( modifiedOn: originTx.modifiedOn, modifiedBy: sender._id } - const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, { - user: receiverInfo._id - }) const msg = control.hierarchy.isDerived(data.mentionedInClass, activity.class.ActivityMessage) ? (await control.findAll(control.ctx, data.mentionedInClass, { _id: data.mentionedIn }))[0] @@ -248,14 +247,11 @@ export async function getPersonNotificationTxes ( await applyNotificationProviders( notificationData, notifyResult, - reference.srcDocId, - reference.srcDocClass, control, res, doc, receiverInfo, senderInfo, - subscriptions, notification.class.MentionInboxNotification, msg as ActivityMessage ) diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index 9f5994e4e21..04f5dc10d54 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -15,16 +15,8 @@ // import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' -import { Analytics } from '@hcengineering/analytics' import chunter, { ChatMessage } from '@hcengineering/chunter' -import contact, { - Employee, - getAvatarProviderId, - getGravatarUrl, - Person, - PersonAccount, - type AvatarInfo -} from '@hcengineering/contact' +import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' import core, { Account, AnyAttribute, @@ -33,7 +25,6 @@ import core, { Class, Collection, combineAttributes, - concatLink, Data, Doc, DocumentUpdate, @@ -64,26 +55,28 @@ import notification, { DocNotifyContext, InboxNotification, MentionInboxNotification, - notificationId, - NotificationType, - PushData, - PushSubscription + NotificationType } from '@hcengineering/notification' -import { getMetadata, getResource, translate } from '@hcengineering/platform' -import serverCore, { type TriggerControl } from '@hcengineering/server-core' +import { getResource, translate } from '@hcengineering/platform' +import { type TriggerControl } from '@hcengineering/server-core' import serverNotification, { getPersonAccountById, NOTIFICATION_BODY_SIZE, - PUSH_NOTIFICATION_TITLE_SIZE, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' -import serverView from '@hcengineering/server-view' import { markupToText, stripTags } from '@hcengineering/text-core' -import { encodeObjectURI } from '@hcengineering/view' -import { workbenchId } from '@hcengineering/workbench' +import { Analytics } from '@hcengineering/analytics' -import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types' +import { + AvailableProvidersCache, + AvailableProvidersCacheKey, + Content, + ContextsCache, + ContextsCacheKey, + NotifyParams, + NotifyResult +} from './types' import { createPullCollaboratorsTx, createPushCollaboratorsTx, @@ -105,6 +98,7 @@ import { updateNotifyContextsSpace, type NotificationProviderControl } from './utils' +import { PushNotificationsHandler } from './push' export function getPushCollaboratorTx ( control: TriggerControl, @@ -165,20 +159,8 @@ export async function getCommonNotificationTxes ( if (notificationTx !== undefined) { const notificationData = TxProcessor.createDoc2Doc(notificationTx) - const subscriptions = await control.findAll(ctx, notification.class.PushSubscription, { user: receiver._id }) - await applyNotificationProviders( - notificationData, - notifyResult, - attachedTo, - attachedToClass, - control, - res, - doc, - receiver, - sender, - subscriptions, - _class - ) + + await applyNotificationProviders(notificationData, notifyResult, control, res, doc, receiver, sender, _class) } return res @@ -383,6 +365,8 @@ export async function pushInboxNotifications ( isViewed: false, docNotifyContext: docNotifyContextId, archived: false, + objectId, + objectClass, ...data } const notificationTx = control.txFactory.createTxCreateDoc(_class, receiver.space, notificationData) @@ -489,156 +473,6 @@ export async function getTranslatedNotificationContent ( return { title: '', body: '' } } -function isReactionMessage (message?: ActivityMessage): boolean { - return ( - message !== undefined && - message._class === activity.class.DocUpdateMessage && - (message as DocUpdateMessage).objectClass === activity.class.Reaction - ) -} - -export async function createPushFromInbox ( - control: TriggerControl, - receiver: ReceiverInfo, - attachedTo: Ref, - attachedToClass: Ref>, - data: Data, - _class: Ref>, - sender: SenderInfo, - _id: Ref, - subscriptions: PushSubscription[], - message?: ActivityMessage -): Promise { - let { title, body } = await getTranslatedNotificationContent(data, _class, control) - if (title === '' || body === '') { - return - } - - title = title.slice(0, PUSH_NOTIFICATION_TITLE_SIZE) - - const senderPerson = sender.person - const linkProviders = control.modelDb.findAllSync(serverView.mixin.ServerLinkIdProvider, {}) - const provider = linkProviders.find(({ _id }) => _id === attachedToClass) - - let id: string = attachedTo - - if (provider !== undefined) { - const encodeFn = await getResource(provider.encode) - const doc = (await control.findAll(control.ctx, attachedToClass, { _id: attachedTo }))[0] - - if (doc === undefined) { - return - } - - id = await encodeFn(doc, control) - } - - const path = [workbenchId, control.workspace.workspaceUrl, notificationId, encodeObjectURI(id, attachedToClass)] - await createPushNotification( - control, - receiver._id as Ref, - title, - body, - _id, - subscriptions, - senderPerson, - path - ) - return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiver.space, { - user: receiver._id, - title, - body, - senderId: sender._id, - tag: _id, - objectId: attachedTo, - objectClass: attachedToClass, - messageId: isReactionMessage(message) ? (message?.attachedTo as Ref) : message?._id, - messageClass: isReactionMessage(message) - ? (message?.attachedToClass as Ref>) - : message?._class, - onClickLocation: { - path - } - }) -} - -export async function createPushNotification ( - control: TriggerControl, - target: Ref, - title: string, - body: string, - _id: string, - subscriptions: PushSubscription[], - senderAvatar?: Data, - path?: string[] -): Promise { - const sesURL: string | undefined = getMetadata(serverNotification.metadata.SesUrl) - const sesAuth: string | undefined = getMetadata(serverNotification.metadata.SesAuthToken) - if (sesURL === undefined || sesURL === '') return - const userSubscriptions = subscriptions.filter((it) => it.user === target) - const data: PushData = { - title, - body - } - if (_id !== undefined) { - data.tag = _id - } - const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? '' - const domainPath = `${workbenchId}/${control.workspace.workspaceUrl}` - data.domain = concatLink(front, domainPath) - if (path !== undefined) { - data.url = concatLink(front, path.join('/')) - } - if (senderAvatar != null) { - const provider = getAvatarProviderId(senderAvatar.avatarType) - if (provider === contact.avatarProvider.Image) { - if (senderAvatar.avatar != null) { - const url = await control.storageAdapter.getUrl(control.ctx, control.workspace, senderAvatar.avatar) - data.icon = url.includes('://') ? url : concatLink(front, url) - } - } else if (provider === contact.avatarProvider.Gravatar && senderAvatar.avatarProps?.url !== undefined) { - data.icon = getGravatarUrl(senderAvatar.avatarProps?.url, 512) - } - } - - void sendPushToSubscription(sesURL, sesAuth, control, target, userSubscriptions, data) -} - -async function sendPushToSubscription ( - sesURL: string, - sesAuth: string | undefined, - control: TriggerControl, - targetUser: Ref, - subscriptions: PushSubscription[], - data: PushData -): Promise { - try { - const result: Ref[] = ( - await ( - await fetch(concatLink(sesURL, '/web-push'), { - method: 'post', - headers: { - 'Content-Type': 'application/json', - ...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {}) - }, - body: JSON.stringify({ - subscriptions, - data - }) - }) - ).json() - ).result - if (result.length > 0) { - const domain = control.hierarchy.findDomain(notification.class.PushSubscription) - if (domain !== undefined) { - await control.lowLevel.clean(control.ctx, domain, result) - } - } - } catch (err) { - control.ctx.info('Cannot send push notification to', { user: targetUser, err }) - } -} - /** * @public */ @@ -682,40 +516,16 @@ export async function pushActivityInboxNotifications ( export async function applyNotificationProviders ( data: InboxNotification, notifyResult: NotifyResult, - attachedTo: Ref, - attachedToClass: Ref>, control: TriggerControl, res: Tx[], object: Doc, receiver: ReceiverInfo, sender: SenderInfo, - subscriptions: PushSubscription[], _class = notification.class.ActivityInboxNotification, message?: ActivityMessage ): Promise { const resources = control.modelDb.findAllSync(serverNotification.class.NotificationProviderResources, {}) for (const [provider, types] of notifyResult.entries()) { - if (provider === notification.providers.PushNotificationProvider) { - // const now = Date.now() - const pushTx = await createPushFromInbox( - control, - receiver, - attachedTo, - attachedToClass, - data, - _class, - sender, - data._id, - subscriptions, - message - ) - if (pushTx !== undefined) { - res.push(pushTx) - } - - continue - } - const resource = resources.find((it) => it.provider === provider) if (resource === undefined) continue @@ -789,8 +599,7 @@ export async function getNotificationTxes ( params: NotifyParams, docNotifyContexts: DocNotifyContext[], activityMessages: ActivityMessage[], - settings: NotificationProviderControl, - subscriptions: PushSubscription[] + settings: NotificationProviderControl ): Promise { if (receiver.account === undefined) { return [] @@ -828,17 +637,23 @@ export async function getNotificationTxes ( if (notificationTx !== undefined) { const notificationData = TxProcessor.createDoc2Doc(notificationTx) + const current: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map() + const providers = Array.from(notifyResult.keys()).filter( + (p) => p !== notification.providers.InboxNotificationProvider + ) + if (providers.length > 0) { + current.set(notificationData._id, providers) + control.contextCache.set('AvailableNotificationProviders', current) + } + await applyNotificationProviders( notificationData, notifyResult, - message.attachedTo, - message.attachedToClass, control, res, object, receiver, sender, - subscriptions, notificationData._class, message ) @@ -1030,9 +845,6 @@ export async function createCollabDocInfo ( } const settings = await getNotificationProviderControl(ctx, control) - const subscriptions = (await control.queryFind(ctx, notification.class.PushSubscription, {})).filter((it) => - targets.has(it.user as Ref) - ) for (const target of targets) { const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target)) @@ -1049,8 +861,7 @@ export async function createCollabDocInfo ( params, notifyContexts, docMessages, - settings, - subscriptions + settings ) const ids = new Set(targetRes.map((it) => it._id)) if (info.account?.email !== undefined) { @@ -2031,6 +1842,7 @@ async function OnDocRemove (txes: TxCUD[], control: TriggerControl): Promis } export * from './types' +export * from './push' export * from './utils' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -2039,7 +1851,8 @@ export default async () => ({ OnAttributeCreate, OnAttributeUpdate, OnDocRemove, - OnEmployeeDeactivate + OnEmployeeDeactivate, + PushNotificationsHandler }, function: { IsUserInFieldValueTypeMatch: isUserInFieldValueTypeMatch, diff --git a/server-plugins/notification-resources/src/push.ts b/server-plugins/notification-resources/src/push.ts new file mode 100644 index 00000000000..1ca94311c07 --- /dev/null +++ b/server-plugins/notification-resources/src/push.ts @@ -0,0 +1,295 @@ +// +// Copyright © 2025 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 serverCore, { TriggerControl } from '@hcengineering/server-core' +import serverNotification, { PUSH_NOTIFICATION_TITLE_SIZE } from '@hcengineering/server-notification' +import { + Account, + Class, + concatLink, + Data, + Doc, + Hierarchy, + Ref, + Tx, + TxCreateDoc, + TxProcessor +} from '@hcengineering/core' +import notification, { + ActivityInboxNotification, + InboxNotification, + MentionInboxNotification, + notificationId, + PushData, + PushSubscription +} from '@hcengineering/notification' +import activity, { ActivityMessage } from '@hcengineering/activity' +import serverView from '@hcengineering/server-view' +import { getMetadata, getResource } from '@hcengineering/platform' +import { workbenchId } from '@hcengineering/workbench' +import { encodeObjectURI } from '@hcengineering/view' +import contact, { + type AvatarInfo, + getAvatarProviderId, + getGravatarUrl, + Person, + PersonAccount, + PersonSpace +} from '@hcengineering/contact' +import { AvailableProvidersCache, AvailableProvidersCacheKey, getTranslatedNotificationContent } from './index' + +async function createPushFromInbox ( + control: TriggerControl, + n: InboxNotification, + receiver: Ref, + receiverSpace: Ref, + subscriptions: PushSubscription[], + senderPerson?: Person +): Promise { + let { title, body } = await getTranslatedNotificationContent(n, n._class, control) + + if (title === '' || body === '') { + return + } + + title = title.slice(0, PUSH_NOTIFICATION_TITLE_SIZE) + + const linkProviders = control.modelDb.findAllSync(serverView.mixin.ServerLinkIdProvider, {}) + const provider = linkProviders.find(({ _id }) => _id === n.objectClass) + + let id: string = n.objectId + + if (provider !== undefined) { + const encodeFn = await getResource(provider.encode) + const cache: Map, Doc> = control.contextCache.get('PushNotificationsHandler') ?? new Map() + const doc = cache.get(n.objectId) ?? (await control.findAll(control.ctx, n.objectClass, { _id: n.objectId }))[0] + + if (doc === undefined) { + return + } + + cache.set(n.objectId, doc) + control.contextCache.set('PushNotificationsHandler', cache) + + id = await encodeFn(doc, control) + } + + const path = [workbenchId, control.workspace.workspaceUrl, notificationId, encodeObjectURI(id, n.objectClass)] + await createPushNotification( + control, + receiver as Ref, + title, + body, + n._id, + subscriptions, + senderPerson, + path + ) + + const messageInfo = getMessageInfo(n, control.hierarchy) + return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiverSpace, { + user: receiver, + title, + body, + senderId: n.createdBy ?? n.modifiedBy, + tag: n._id, + objectId: n.objectId, + objectClass: n.objectClass, + messageId: messageInfo._id, + messageClass: messageInfo._class, + onClickLocation: { + path + } + }) +} + +function getMessageInfo ( + n: InboxNotification, + hierarchy: Hierarchy +): { + _id?: Ref + _class?: Ref> + } { + if (hierarchy.isDerived(n._class, notification.class.ActivityInboxNotification)) { + const activityNotification = n as ActivityInboxNotification + + if ( + activityNotification.attachedToClass === activity.class.DocUpdateMessage && + hierarchy.isDerived(activityNotification.objectClass, activity.class.ActivityMessage) + ) { + return { + _id: activityNotification.objectId as Ref, + _class: activityNotification.objectClass + } + } + + return { + _id: activityNotification.attachedTo, + _class: activityNotification.attachedToClass + } + } + + if (hierarchy.isDerived(n._class, notification.class.MentionInboxNotification)) { + const mentionNotification = n as MentionInboxNotification + if (hierarchy.isDerived(mentionNotification.mentionedInClass, activity.class.ActivityMessage)) { + return { + _id: mentionNotification.mentionedIn as Ref, + _class: mentionNotification.mentionedInClass + } + } + } + + return {} +} + +export async function createPushNotification ( + control: TriggerControl, + target: Ref, + title: string, + body: string, + _id: string, + subscriptions: PushSubscription[], + senderAvatar?: Data, + path?: string[] +): Promise { + const sesURL: string | undefined = getMetadata(serverNotification.metadata.SesUrl) + const sesAuth: string | undefined = getMetadata(serverNotification.metadata.SesAuthToken) + if (sesURL === undefined || sesURL === '') return + const userSubscriptions = subscriptions.filter((it) => it.user === target) + const data: PushData = { + title, + body + } + if (_id !== undefined) { + data.tag = _id + } + const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? '' + const domainPath = `${workbenchId}/${control.workspace.workspaceUrl}` + data.domain = concatLink(front, domainPath) + if (path !== undefined) { + data.url = concatLink(front, path.join('/')) + } + if (senderAvatar != null) { + const provider = getAvatarProviderId(senderAvatar.avatarType) + if (provider === contact.avatarProvider.Image) { + if (senderAvatar.avatar != null) { + const url = await control.storageAdapter.getUrl(control.ctx, control.workspace, senderAvatar.avatar) + data.icon = url.includes('://') ? url : concatLink(front, url) + } + } else if (provider === contact.avatarProvider.Gravatar && senderAvatar.avatarProps?.url !== undefined) { + data.icon = getGravatarUrl(senderAvatar.avatarProps?.url, 512) + } + } + + void sendPushToSubscription(sesURL, sesAuth, control, target, userSubscriptions, data) +} + +async function sendPushToSubscription ( + sesURL: string, + sesAuth: string | undefined, + control: TriggerControl, + targetUser: Ref, + subscriptions: PushSubscription[], + data: PushData +): Promise { + try { + const result: Ref[] = ( + await ( + await fetch(concatLink(sesURL, '/web-push'), { + method: 'post', + headers: { + 'Content-Type': 'application/json', + ...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {}) + }, + body: JSON.stringify({ + subscriptions, + data + }) + }) + ).json() + ).result + if (result.length > 0) { + const domain = control.hierarchy.findDomain(notification.class.PushSubscription) + if (domain !== undefined) { + await control.lowLevel.clean(control.ctx, domain, result) + } + } + } catch (err) { + control.ctx.info('Cannot send push notification to', { user: targetUser, err }) + } +} + +export async function PushNotificationsHandler ( + txes: TxCreateDoc[], + control: TriggerControl +): Promise { + const availableProviders: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map() + + const all: InboxNotification[] = txes + .map((tx) => TxProcessor.createDoc2Doc(tx)) + .filter( + (it) => + availableProviders.get(it._id)?.find((p) => p === notification.providers.PushNotificationProvider) !== undefined + ) + + if (all.length === 0) { + return [] + } + + const receivers = new Set(all.map((it) => it.user)) + const subscriptions = (await control.queryFind(control.ctx, notification.class.PushSubscription, {})).filter((it) => + receivers.has(it.user) + ) + + if (subscriptions.length === 0) { + return [] + } + + const senders = Array.from(new Set(all.map((it) => it.createdBy))) + const senderAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { + _id: { $in: senders as Ref[] } + }) + const senderPersons = await control.findAll(control.ctx, contact.class.Person, { + _id: { $in: Array.from(new Set(senderAccounts.map((it) => it.person))) } + }) + + const res: Tx[] = [] + + for (const inboxNotification of all) { + const { user } = inboxNotification + const userSubscriptions = subscriptions.filter((it) => it.user === user) + if (userSubscriptions.length === 0) continue + + const senderAccount = senderAccounts.find( + (it) => it._id === (inboxNotification.createdBy ?? inboxNotification.modifiedBy) + ) + const senderPerson = + senderAccount !== undefined ? senderPersons.find((it) => it._id === senderAccount.person) : undefined + const tx = await createPushFromInbox( + control, + inboxNotification, + user, + inboxNotification.space, + userSubscriptions, + senderPerson + ) + + if (tx !== undefined) { + res.push(tx) + } + } + + return res +} diff --git a/server-plugins/notification-resources/src/types.ts b/server-plugins/notification-resources/src/types.ts index 40dbf49b3c1..5de88955f8d 100644 --- a/server-plugins/notification-resources/src/types.ts +++ b/server-plugins/notification-resources/src/types.ts @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import { BaseNotificationType, DocNotifyContext, NotificationProvider } from '@hcengineering/notification' +import { + BaseNotificationType, + DocNotifyContext, + InboxNotification, + NotificationProvider +} from '@hcengineering/notification' import { Ref } from '@hcengineering/core' /** @@ -39,3 +44,6 @@ export const ContextsCacheKey = 'DocNotifyContexts' export interface ContextsCache { contexts: Map> } + +export const AvailableProvidersCacheKey = 'AvailableNotificationProviders' +export type AvailableProvidersCache = Map, Ref[]> diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index 271692f4783..15ba6d8ec91 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -661,3 +661,11 @@ export async function getObjectSpace (control: TriggerControl, doc: Doc, cache: : (cache.get(doc.space) as Space) ?? (await control.findAll(control.ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0] } + +export function isReactionMessage (message?: ActivityMessage): boolean { + return ( + message !== undefined && + message._class === activity.class.DocUpdateMessage && + (message as DocUpdateMessage).objectClass === activity.class.Reaction + ) +} diff --git a/server-plugins/notification/src/index.ts b/server-plugins/notification/src/index.ts index 1382bb3c722..918a73a01a9 100644 --- a/server-plugins/notification/src/index.ts +++ b/server-plugins/notification/src/index.ts @@ -168,7 +168,8 @@ export default plugin(serverNotificationId, { OnAttributeUpdate: '' as Resource, OnReactionChanged: '' as Resource, OnDocRemove: '' as Resource, - OnEmployeeDeactivate: '' as Resource + OnEmployeeDeactivate: '' as Resource, + PushNotificationsHandler: '' as Resource }, function: { IsUserInFieldValueTypeMatch: '' as TypeMatchFunc, diff --git a/server-plugins/request-resources/src/index.ts b/server-plugins/request-resources/src/index.ts index 4c06b8a7c67..235b304e23f 100644 --- a/server-plugins/request-resources/src/index.ts +++ b/server-plugins/request-resources/src/index.ts @@ -161,10 +161,7 @@ async function getRequestNotificationTx ( } const notificationControl = await getNotificationProviderControl(ctx, control) - const collaboratorsSet = new Set(collaborators) - const subscriptions = (await control.queryFind(control.ctx, notification.class.PushSubscription, {})).filter((it) => - collaboratorsSet.has(it.user) - ) + for (const target of collaborators) { const targetInfo = toReceiverInfo(control.hierarchy, usersInfo.get(target)) if (targetInfo === undefined) continue @@ -179,8 +176,7 @@ async function getRequestNotificationTx ( { isOwn: true, isSpace: false, shouldUpdateTimestamp: true }, notifyContexts, messages, - notificationControl, - subscriptions + notificationControl ) res.push(...txes) } diff --git a/services/github/pod-github/src/notifications.ts b/services/github/pod-github/src/notifications.ts index a238127c1bf..893d971b5b4 100644 --- a/services/github/pod-github/src/notifications.ts +++ b/services/github/pod-github/src/notifications.ts @@ -36,6 +36,8 @@ export async function createNotification ( } else { await client.createDoc(notification.class.CommonInboxNotification, data.space, { user: data.user, + objectId: forDoc._id, + objectClass: forDoc._class, icon: github.icon.Github, message: data.message, props: data.props,