Reduce finds on create notifications (#8352)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2025-03-26 22:20:20 +04:00 committed by GitHub
parent 651c1395a8
commit af1563f580
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 243 additions and 321 deletions

View File

@ -48,6 +48,7 @@ import {
getTextPresenter,
removeDocInboxNotifications
} from '@hcengineering/server-notification-resources'
import { Person } from '@hcengineering/contact'
import { ReferenceTrigger } from './references'
import { getAttrName, getCollectionAttribute, getDocUpdateAction, getTxAttributesUpdates } from './utils'
@ -417,7 +418,7 @@ async function OnDocRemoved (txes: TxCUD<Doc>[], control: TriggerControl): Promi
async function ReactionNotificationContentProvider (
doc: ActivityMessage,
originTx: TxCUD<Doc>,
_: PersonId,
_: Ref<Person>,
control: TriggerControl
): Promise<NotificationContent> {
const tx = originTx as TxCreateDoc<Reaction>

View File

@ -14,7 +14,7 @@
//
import activity, { ActivityMessage, ActivityReference, UserMentionInfo } from '@hcengineering/activity'
import contact, { Employee, Person, pickPrimarySocialId } from '@hcengineering/contact'
import contact, { Employee, Person } from '@hcengineering/contact'
import core, {
PersonId,
Blob,
@ -47,11 +47,10 @@ import {
getNotificationContent,
getNotificationProviderControl,
getPushCollaboratorTx,
isShouldNotifyTx,
NotifyResult,
shouldNotifyCommon,
toReceiverInfo,
type NotificationProviderControl
type NotificationProviderControl,
isShouldNotifyTx
} from '@hcengineering/server-notification-resources'
import { areEqualJson, extractReferences, jsonToMarkup, markupToJSON } from '@hcengineering/text-core'
@ -79,27 +78,31 @@ export async function getPersonNotificationTxes (
originTx: TxCUD<Doc>,
notificationControl: NotificationProviderControl
): Promise<Tx[]> {
const receiver = reference.attachedTo as Ref<Person>
const receiverSocialIds = await control.findAll(ctx, contact.class.SocialIdentity, { attachedTo: receiver })
const receiverSocialStrings = receiverSocialIds.map((si) => si._id) as PersonId[]
const receiverPersonRef = reference.attachedTo as Ref<Person>
const receiverSocialIdentity = await control.findAll(ctx, contact.class.SocialIdentity, {
attachedTo: receiverPersonRef
})
const receiverSocialIds = receiverSocialIdentity.map((si) => si._id) as PersonId[]
if (receiverSocialStrings.includes(senderId)) {
if (receiverSocialIds.includes(senderId)) {
return []
}
const employee = await control.findAll(
ctx,
contact.mixin.Employee,
{ _id: receiver as Ref<Employee>, active: true },
{ limit: 1 }
)
const account = employee[0]?.personUuid
if (account == null) {
const receiverEmployee = (
await control.findAll(
ctx,
contact.mixin.Employee,
{ _id: receiverPersonRef as Ref<Employee>, active: true },
{ limit: 1 }
)
)[0]
const receiverAccount = receiverEmployee?.personUuid
if (receiverAccount == null) {
return []
}
const res: Tx[] = []
const isAvailable = await checkSpace(account, space, control, res)
const isAvailable = await checkSpace(receiverAccount, space, control, res)
if (!isAvailable) {
return []
@ -107,10 +110,12 @@ export async function getPersonNotificationTxes (
const doc = (await control.findAll(ctx, reference.srcDocClass, { _id: reference.srcDocId }))[0]
const receiverSpace = (await control.findAll(ctx, contact.class.PersonSpace, { person: receiver }, { limit: 1 }))[0]
const receiverSpace = (
await control.findAll(ctx, contact.class.PersonSpace, { person: receiverPersonRef }, { limit: 1 })
)[0]
if (receiverSpace === undefined) return res
const collaboratorsTx = await getCollaboratorsTxes(reference, control, account, doc)
const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiverAccount, doc)
res.push(...collaboratorsTx)
@ -120,7 +125,7 @@ export async function getPersonNotificationTxes (
const info = (
await control.findAll<UserMentionInfo>(ctx, activity.class.UserMentionInfo, {
user: receiver,
user: receiverPersonRef,
attachedTo: reference.attachedDocId
})
)[0]
@ -130,7 +135,7 @@ export async function getPersonNotificationTxes (
control.txFactory.createTxCreateDoc(activity.class.UserMentionInfo, space, {
attachedTo: reference.attachedDocId ?? reference.srcDocId,
attachedToClass: reference.attachedDocClass ?? reference.srcDocClass,
user: receiver,
user: receiverPersonRef,
content: reference.message,
collection: 'mentions'
})
@ -150,42 +155,35 @@ export async function getPersonNotificationTxes (
mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass,
objectId: reference.srcDocId,
objectClass: reference.srcDocClass,
user: account,
user: receiverAccount,
isViewed: false,
archived: false
}
const senderPerson = await getPerson(control, senderId)
const senderSocialIds =
senderPerson !== undefined
? await control.findAll(ctx, contact.class.SocialIdentity, { attachedTo: senderPerson._id })
: []
const receiverSocialString = pickPrimarySocialId(receiverSocialStrings)
const receiverInfo = toReceiverInfo(control.hierarchy, {
_id: receiverSocialString,
person: employee[0],
const receiver = {
account: receiverAccount,
socialIds: receiverSocialIds,
space: receiverSpace._id,
socialStrings: receiverSocialStrings
})
if (receiverInfo === undefined) return res
const senderInfo = {
_id: senderId,
person: senderPerson,
socialStrings: senderSocialIds.map((si) => si._id)
employee: receiverEmployee._id
}
const sender = {
socialId: senderId,
person: senderPerson
}
const notifyResult = await shouldNotifyCommon(
control,
receiverSocialStrings,
receiverSocialIds,
notification.ids.MentionCommonNotificationType,
notificationControl
)
const messageNotifyResult = await getMessageNotifyResult(
reference,
account,
receiverSocialStrings,
receiverAccount,
receiverEmployee,
receiverSocialIds,
control,
originTx,
doc,
@ -204,8 +202,8 @@ export async function getPersonNotificationTxes (
control,
doc,
data,
receiverInfo,
senderInfo,
receiver,
sender,
reference.srcDocId,
reference.srcDocClass,
doc.space,
@ -220,12 +218,12 @@ export async function getPersonNotificationTxes (
await control.findAll(
ctx,
notification.class.DocNotifyContext,
{ objectId: reference.srcDocId, user: account },
{ objectId: reference.srcDocId, user: receiverAccount },
{ projection: { _id: 1 } }
)
)[0]
if (context !== undefined) {
const content = await getNotificationContent(originTx, receiverSocialStrings, senderInfo, doc, control)
const content = await getNotificationContent(originTx, receiverPersonRef, sender, doc, control)
const notificationData: CommonInboxNotification = {
...data,
...content,
@ -246,8 +244,8 @@ export async function getPersonNotificationTxes (
control,
res,
doc,
receiverInfo,
senderInfo,
receiver,
sender,
notification.class.MentionInboxNotification,
msg as ActivityMessage
)
@ -331,6 +329,7 @@ async function getCollaboratorsTxes (
async function getMessageNotifyResult (
reference: Data<ActivityReference>,
account: AccountUuid,
person: Person,
personIds: PersonId[],
control: TriggerControl,
tx: TxCUD<Doc>,
@ -357,7 +356,7 @@ async function getMessageNotifyResult (
return new Map()
}
return await isShouldNotifyTx(control, tx, doc, personIds, false, false, notificationControl, undefined)
return await isShouldNotifyTx(control, tx, doc, person._id, personIds, false, false, notificationControl, undefined)
}
function isMarkupType (type: Ref<Class<Type<any>>>): boolean {

View File

@ -307,7 +307,7 @@ export async function ChunterTrigger (txes: TxCUD<Doc>[], control: TriggerContro
export async function getChunterNotificationContent (
_: Doc,
tx: TxCUD<Doc>,
target: PersonId,
target: Ref<Person>,
control: TriggerControl
): Promise<NotificationContent> {
let title: IntlString = notification.string.CommonNotificationTitle
@ -494,7 +494,7 @@ async function OnUserStatus (txes: TxCUD<UserStatus>[], control: TriggerControl)
return []
}
function JoinChannelTypeMatch (originTx: Tx, _: Doc, person: Person, user: PersonId[]): boolean {
function JoinChannelTypeMatch (originTx: Tx, _: Doc, person: Ref<Person>, user: PersonId[]): boolean {
if (user.includes(originTx.modifiedBy)) return false
if (originTx._class !== core.class.TxUpdateDoc) return false

View File

@ -202,6 +202,16 @@ export async function getPrimarySocialIdsByAccounts (
}
export async function getAccountBySocialId (control: TriggerControl, socialId: PersonId): Promise<AccountUuid | null> {
const contextAccount = control.ctx.contextData.socialStringsToUsers.get(socialId)
if (contextAccount != null) {
return contextAccount
}
const controlAccount = control.ctx.contextData.account
if (controlAccount.socialIds.includes(socialId)) {
return controlAccount.uuid
}
const socialIdentity = await control.findAll(
control.ctx,
contact.class.SocialIdentity,

View File

@ -6,7 +6,6 @@ import core, {
AccountRole,
combineAttributes,
DocumentQuery,
includesAny,
PersonId,
Ref,
SortingOrder,
@ -21,7 +20,7 @@ import core, {
systemAccountUuid
} from '@hcengineering/core'
import { NotificationType } from '@hcengineering/notification'
import { getEmployees, getSocialStrings, getSocialStringsByPersons } from '@hcengineering/server-contact'
import { getEmployees, getSocialStrings } from '@hcengineering/server-contact'
import { TriggerControl } from '@hcengineering/server-core'
import documents, {
@ -391,7 +390,7 @@ export async function documentTextPresenter (doc: ControlledDocument): Promise<s
async function CoAuthorsTypeMatch (
originTx: TxCUD<ControlledDocument>,
_doc: Doc,
person: Person,
person: Ref<Person>,
socialIds: PersonId[],
_type: NotificationType,
control: TriggerControl
@ -402,15 +401,13 @@ async function CoAuthorsTypeMatch (
const employees = Array.isArray(tx.operations.coAuthors)
? tx.operations.coAuthors ?? []
: (combineAttributes([tx.operations], 'coAuthors', '$push', '$each') as Ref<Employee>[])
const employeeSocialStrings = Object.values(await getSocialStringsByPersons(control, employees)).flat()
return includesAny(socialIds, employeeSocialStrings)
return employees.some((it) => it === person)
} else if (originTx._class === core.class.TxCreateDoc) {
const tx = originTx as TxCreateDoc<ControlledDocument>
const employees = tx.attributes.coAuthors
const employeeSocialStrings = Object.values(await getSocialStringsByPersons(control, employees)).flat()
return includesAny(socialIds, employeeSocialStrings)
return employees.some((it) => it === person)
}
return false

View File

@ -95,7 +95,7 @@ export async function OnMessageCreate (txes: Tx[], control: TriggerControl): Pro
export function IsIncomingMessageTypeMatch (
tx: Tx,
doc: Doc,
person: Person,
person: Ref<Person>,
user: PersonId[],
type: NotificationType,
control: TriggerControl

View File

@ -28,7 +28,6 @@ import core, {
Data,
Doc,
DocumentUpdate,
generateId,
MeasureContext,
MixinUpdate,
Ref,
@ -36,7 +35,6 @@ import core, {
SortingOrder,
Space,
Timestamp,
toIdMap,
Tx,
TxCreateDoc,
TxCUD,
@ -45,12 +43,13 @@ import core, {
TxRemoveDoc,
TxUpdateDoc,
AccountUuid,
notEmpty
notEmpty,
generateId,
toIdMap
} from '@hcengineering/core'
import notification, {
ActivityInboxNotification,
BaseNotificationType,
BrowserNotification,
ClassCollaborators,
Collaborators,
CommonInboxNotification,
@ -61,13 +60,6 @@ import notification, {
} from '@hcengineering/notification'
import { getResource, translate } from '@hcengineering/platform'
import { type TriggerControl } from '@hcengineering/server-core'
import {
getEmployeeByAcc,
getPrimarySocialIdsByAccounts,
getAccountBySocialId,
getSocialIdsByAccounts,
getEmployeesBySocialIds
} from '@hcengineering/server-contact'
import serverNotification, {
NOTIFICATION_BODY_SIZE,
ReceiverInfo,
@ -75,6 +67,7 @@ import serverNotification, {
} from '@hcengineering/server-notification'
import { markupToText, stripTags } from '@hcengineering/text-core'
import { Analytics } from '@hcengineering/analytics'
import { getAccountBySocialId, getEmployeesBySocialIds } from '@hcengineering/server-contact'
import {
AvailableProvidersCache,
@ -89,22 +82,21 @@ import {
createPullCollaboratorsTx,
createPushCollaboratorsTx,
getHTMLPresenter,
getNotificationContent,
getNotificationLink,
getNotificationProviderControl,
getObjectSpace,
getTextPresenter,
getUsersInfo,
getReceiversInfo,
isAllowed,
isMixinTx,
isShouldNotifyTx,
isUserEmployeeInFieldValueTypeMatch,
isUserInFieldValueTypeMatch,
messageToMarkup,
replaceAll,
toReceiverInfo,
updateNotifyContextsSpace,
type NotificationProviderControl
type NotificationProviderControl,
getNotificationContent,
getSenderInfo
} from './utils'
import { PushNotificationsHandler } from './push'
@ -381,7 +373,7 @@ export async function pushInboxNotifications (
objectClass,
objectSpace,
receiver,
sender._id,
sender.socialId,
shouldUpdateTimestamp ? modifiedOn : undefined,
tx
)
@ -517,7 +509,7 @@ export async function pushActivityInboxNotifications (
activityMessage: ActivityMessage,
shouldUpdateTimestamp: boolean
): Promise<TxCreateDoc<InboxNotification> | undefined> {
const content = await getNotificationContent(originTx, receiver.socialStrings, sender, object, control)
const content = await getNotificationContent(originTx, receiver.employee, sender, object, control)
const data: Partial<Data<ActivityInboxNotification>> = {
...content,
attachedTo: activityMessage._id,
@ -582,7 +574,7 @@ async function createNotifyContext (
const contextsCache: ContextsCache = control.cache.get(ContextsCacheKey) ?? {
contexts: new Map<string, Ref<DocNotifyContext>>()
}
const cacheKey = `${objectId}_${receiver._id}`
const cacheKey = `${objectId}_${receiver.account}`
const cachedId = contextsCache.contexts.get(cacheKey)
if (cachedId !== undefined) {
@ -602,18 +594,15 @@ async function createNotifyContext (
hidden: false,
tx: tx?._id,
lastUpdateTimestamp: updateTimestamp,
lastViewedTimestamp: sender === receiver._id ? updateTimestamp : undefined
lastViewedTimestamp: receiver.socialIds.some((it) => it === sender) ? updateTimestamp : undefined
})
contextsCache.contexts.set(cacheKey, createTx.objectId)
control.cache.set(ContextsCacheKey, contextsCache)
await ctx.with('apply', {}, () => control.apply(control.ctx, [createTx]))
const personUuid = receiver.person?.personUuid
if (personUuid !== undefined) {
control.ctx.contextData.broadcast.targets['docNotifyContext' + createTx._id] = (it) => {
if (it._id === createTx._id) {
return [personUuid]
}
control.ctx.contextData.broadcast.targets['docNotifyContext' + createTx._id] = (it) => {
if (it._id === createTx._id) {
return [receiver.account]
}
}
return createTx.objectId
@ -631,10 +620,6 @@ export async function getNotificationTxes (
activityMessages: ActivityMessage[],
settings: NotificationProviderControl
): Promise<Tx[]> {
if (receiver.employee === undefined) {
return []
}
const res: Tx[] = []
for (const message of activityMessages) {
@ -643,7 +628,8 @@ export async function getNotificationTxes (
control,
tx,
object,
receiver.socialStrings,
receiver.employee,
receiver.socialIds,
params.isOwn,
params.isSpace,
settings,
@ -699,7 +685,7 @@ export async function getNotificationTxes (
message.attachedToClass,
object.space,
receiver,
sender._id,
sender.socialId,
params.shouldUpdateTimestamp ? tx.modifiedOn : undefined,
tx
)
@ -718,19 +704,15 @@ async function updateContextsTimestamp (
): Promise<void> {
if (contexts.length === 0) return
const res: Tx[] = []
const socialIdsByAccounts = await getSocialIdsByAccounts(
control,
contexts.map((it) => it.user)
)
const modifiedByAccount = await getAccountBySocialId(control, modifiedBy)
for (const context of contexts) {
const isViewed =
context.lastViewedTimestamp !== undefined && (context.lastUpdateTimestamp ?? 0) <= context.lastViewedTimestamp
const ctxUserSocialIds = socialIdsByAccounts[context.user] ?? []
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
hidden: false,
lastUpdateTimestamp: timestamp,
...(isViewed && ctxUserSocialIds.includes(modifiedBy)
...(isViewed && context.user === modifiedByAccount
? {
lastViewedTimestamp: timestamp
}
@ -867,29 +849,18 @@ export async function createCollabDocInfo (
if (targets.size === 0) {
return res
}
const targetPrimarySocialStringsByAccounts = await getPrimarySocialIdsByAccounts(control, Array.from(targets))
const usersInfo = await ctx.with('get-user-info', {}, (ctx) =>
getUsersInfo(ctx, [...Object.values(targetPrimarySocialStringsByAccounts), tx.modifiedBy], control)
)
const sender: SenderInfo = usersInfo.get(tx.modifiedBy) ?? {
_id: tx.modifiedBy,
socialStrings: []
}
const receivers = await getReceiversInfo(ctx, Array.from(targets), control)
const sender: SenderInfo = await getSenderInfo(ctx, tx.modifiedBy, control)
const settings = await getNotificationProviderControl(ctx, control)
for (const target of targets) {
const targetSocialString = targetPrimarySocialStringsByAccounts[target]
const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(targetSocialString))
if (info === undefined) continue
for (const receiver of receivers) {
const targetRes = await getNotificationTxes(
ctx,
control,
object,
tx,
info,
receiver,
sender,
params,
notifyContexts,
@ -897,13 +868,10 @@ export async function createCollabDocInfo (
settings
)
const ids = new Set(targetRes.map((it) => it._id))
const { personUuid } = info.person
if (personUuid !== undefined) {
const id = generateId() as string
control.ctx.contextData.broadcast.targets[id] = (it) => {
if (ids.has(it._id)) {
return [personUuid]
}
const id = generateId() as string
control.ctx.contextData.broadcast.targets[id] = (it) => {
if (ids.has(it._id)) {
return [receiver.account]
}
}
res = res.concat(targetRes)
@ -1141,13 +1109,15 @@ async function updateCollaboratorsMixin (
}
const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {})
const modifiedByAccount = await getAccountBySocialId(control, tx.modifiedBy)
const socialIdsByAccounts = await getSocialIdsByAccounts(control, tx.attributes.collaborators)
const sender: SenderInfo = await getSenderInfo(ctx, tx.modifiedBy, control)
const receivers = await getReceiversInfo(ctx, tx.attributes.collaborators, control)
for (const collab of tx.attributes.collaborators) {
if (!prevCollabs.has(collab) && modifiedByAccount !== collab) {
const info = receivers.find((it) => it.account === collab)
if (info === undefined) continue
if (!prevCollabs.has(collab) && sender.person?.personUuid !== collab) {
for (const provider of providers) {
if (isAllowed(control, socialIdsByAccounts[collab], type, provider, notificationControl)) {
if (isAllowed(control, info.socialIds, type, provider, notificationControl)) {
newCollabs.push(collab)
break
}
@ -1167,23 +1137,17 @@ async function updateCollaboratorsMixin (
user: { $in: newCollabs },
objectId: tx.objectId
})
const newCollabsPrimarySocialStringsByAccounts = await getPrimarySocialIdsByAccounts(control, newCollabs)
const infos = await ctx.with('get-user-info', {}, (ctx) =>
getUsersInfo(ctx, [...Object.values(newCollabsPrimarySocialStringsByAccounts), originTx.modifiedBy], control)
)
const sender: SenderInfo = infos.get(originTx.modifiedBy) ?? { _id: originTx.modifiedBy, socialStrings: [] }
const infos = receivers.filter((it) => newCollabs.includes(it.account))
for (const collab of newCollabs) {
const target = toReceiverInfo(hierarchy, infos.get(newCollabsPrimarySocialStringsByAccounts[collab]))
if (target === undefined) continue
const isMember = space.members.includes(collab)
for (const target of infos) {
const isMember = space.members.includes(target.account)
if (space.private && !isMember) continue
if (!hierarchy.isDerived(space._class, core.class.SystemSpace) && !isMember) {
res.push(
control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
$push: { members: collab }
$push: { members: target.account }
})
)
}
@ -1536,12 +1500,7 @@ export async function OnAttributeUpdate (txes: Tx[], control: TriggerControl): P
return result
}
async function applyUserTxes (
ctx: MeasureContext,
control: TriggerControl,
txes: Tx[],
cache: Map<AccountUuid, Doc> = new Map<AccountUuid, Doc>()
): Promise<Tx[]> {
async function applyUserTxes (ctx: MeasureContext, control: TriggerControl, txes: Tx[]): Promise<Tx[]> {
const map: Map<AccountUuid, Tx[]> = new Map<AccountUuid, Tx[]>()
const res: Tx[] = []
@ -1553,17 +1512,6 @@ async function applyUserTxes (
) {
const notification = TxProcessor.createDoc2Doc(ttx as TxCreateDoc<InboxNotification>)
if (map.has(notification.user)) {
map.get(notification.user)?.push(tx)
} else {
map.set(notification.user, [tx])
}
} else if (
control.hierarchy.isDerived(ttx.objectClass, notification.class.BrowserNotification) &&
ttx._class === core.class.TxCreateDoc
) {
const notification = TxProcessor.createDoc2Doc(ttx as TxCreateDoc<BrowserNotification>)
if (map.has(notification.user)) {
map.get(notification.user)?.push(tx)
} else {
@ -1575,18 +1523,11 @@ async function applyUserTxes (
}
for (const [user, txs] of map.entries()) {
const person = (cache.get(user) as Person) ?? (await getEmployeeByAcc(control, user))
const personUuid = person?.personUuid
if (personUuid !== undefined) {
cache.set(user, person)
await control.apply(ctx, txs)
const m1 = toIdMap(txs)
control.ctx.contextData.broadcast.targets.docNotifyContext = (it) => {
if (m1.has(it._id)) {
return [personUuid]
}
await control.apply(ctx, txs)
const m1 = toIdMap(txs)
control.ctx.contextData.broadcast.targets.docNotifyContext = (it) => {
if (m1.has(it._id)) {
return [user]
}
}
}
@ -1640,12 +1581,9 @@ async function updateCollaborators (
if (hierarchy.classHierarchyMixin(objectClass, activity.mixin.ActivityDoc) === undefined) return res
const contexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, { objectId })
const toAddPrimarySocialStringsByAccounts = await getPrimarySocialIdsByAccounts(control, toAdd)
const addedInfo = await getUsersInfo(ctx, Object.values(toAddPrimarySocialStringsByAccounts), control)
const addedInfo = await getReceiversInfo(ctx, toAdd, control)
for (const addedUser of addedInfo.values()) {
const info = toReceiverInfo(hierarchy, addedUser)
if (info === undefined) continue
for (const info of addedInfo.values()) {
const context = getDocNotifyContext(control, contexts, objectId, info.account)
if (context !== undefined) {
if (context.hidden) {
@ -1884,7 +1822,6 @@ export default async () => ({
PushNotificationsHandler
},
function: {
IsUserInFieldValueTypeMatch: isUserInFieldValueTypeMatch,
IsUserEmployeeInFieldValueTypeMatch: isUserEmployeeInFieldValueTypeMatch
}
})

View File

@ -15,9 +15,17 @@
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
import { Analytics } from '@hcengineering/analytics'
import chunter, { ChatMessage } from '@hcengineering/chunter'
import contact, { Employee, formatName, includesAny, Person } from '@hcengineering/contact'
import contact, {
Employee,
formatName,
includesAny,
Person,
PersonSpace,
SocialIdentity,
SocialIdentityRef
} from '@hcengineering/contact'
import core, {
PersonId,
AccountUuid,
Class,
concatLink,
Doc,
@ -26,15 +34,16 @@ import core, {
Hierarchy,
Markup,
matchQuery,
type MeasureContext,
MixinUpdate,
notEmpty,
PersonId,
Ref,
Space,
Tx,
TxCUD,
TxMixin,
TxUpdateDoc,
type MeasureContext,
AccountUuid
TxUpdateDoc
} from '@hcengineering/core'
import notification, {
BaseNotificationType,
@ -43,18 +52,12 @@ import notification, {
NotificationContent,
notificationId,
NotificationProvider,
NotificationType,
type NotificationProviderSetting,
NotificationType,
type NotificationTypeSetting
} from '@hcengineering/notification'
import { getMetadata, getResource, IntlString, translate } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import {
getPersonsBySocialIds,
getEmployeesBySocialIds,
getSocialStringsByPersons,
getPerson
} from '@hcengineering/server-contact'
import serverNotification, {
HTMLPresenter,
NotificationPresenter,
@ -74,7 +77,7 @@ import { NotifyResult } from './types'
export function isUserEmployeeInFieldValueTypeMatch (
_: Tx,
doc: Doc,
person: Person,
person: Ref<Person>,
socialIds: PersonId[],
type: NotificationType,
control: TriggerControl
@ -145,13 +148,13 @@ export async function shouldNotifyCommon (
export function isAllowed (
control: TriggerControl,
receiver: PersonId[],
receiverIds: PersonId[],
type: BaseNotificationType,
provider: NotificationProvider,
notificationControl: NotificationProviderControl
): boolean {
const providerSettings = (notificationControl.byProvider.get(provider._id) ?? []).filter(({ createdBy }) =>
createdBy !== undefined ? receiver.includes(createdBy) : false
createdBy !== undefined ? receiverIds.includes(createdBy) : false
)
if (providerSettings.length > 0 && providerSettings.every((s) => !s.enabled)) {
@ -169,7 +172,7 @@ export function isAllowed (
}
const setting = (notificationControl.settingsByProvider.get(provider._id) ?? []).find(
(it) => it.type === type._id && it.createdBy !== undefined && receiver.includes(it.createdBy)
(it) => it.type === type._id && it.createdBy !== undefined && receiverIds.includes(it.createdBy)
)
if (setting !== undefined) {
@ -189,6 +192,7 @@ export async function isShouldNotifyTx (
control: TriggerControl,
tx: TxCUD<Doc>,
object: Doc,
person: Ref<Person>,
personIds: PersonId[],
isOwn: boolean,
isSpace: boolean,
@ -213,8 +217,6 @@ export async function isShouldNotifyTx (
const mixin = control.hierarchy.as(type, serverNotification.mixin.TypeMatch)
if (mixin.func !== undefined) {
const f = await getResource(mixin.func)
const person = await getPerson(control, personIds[0])
if (person === undefined) continue
let res = f(tx, object, person, personIds, type, control)
if (res instanceof Promise) {
res = await res
@ -324,15 +326,15 @@ export function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy)
}
async function getSenderName (control: TriggerControl, sender: SenderInfo): Promise<string> {
if (sender._id === core.account.System || sender._id === core.account.ConfigUser) {
if (sender.socialId === core.account.System || sender.socialId === core.account.ConfigUser) {
return await translate(core.string.System, {})
}
const { person } = sender
if (person === undefined) {
console.error('Cannot find person', { accountId: sender._id })
Analytics.handleError(new Error(`Cannot find person ${sender._id}`))
console.error('Cannot find person', { socialId: sender.socialId })
Analytics.handleError(new Error(`Cannot find person ${sender.socialId}`))
return ''
}
@ -407,7 +409,7 @@ function getNotificationPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy
export async function getNotificationContent (
originTx: TxCUD<Doc>,
socialIds: PersonId[],
receiver: Ref<Person>,
sender: SenderInfo,
object: Doc,
control: TriggerControl
@ -425,7 +427,7 @@ export async function getNotificationContent (
if (notificationPresenter !== undefined) {
const getFuillfillmentParams = await getResource(notificationPresenter.presenter)
const updateParams = await getFuillfillmentParams(object, originTx, socialIds[0], control)
const updateParams = await getFuillfillmentParams(object, originTx, receiver, control)
title = updateParams.title
body = updateParams.body
data = updateParams.data
@ -455,74 +457,93 @@ export async function getNotificationContent (
return content
}
export async function getUsersInfo (
export async function getReceiversInfo (
ctx: MeasureContext,
ids: PersonId[],
accounts: AccountUuid[],
control: TriggerControl
): Promise<Map<PersonId, ReceiverInfo | SenderInfo>> {
if (ids.length === 0) return new Map()
const uniqueIds = Array.from(new Set(ids))
): Promise<ReceiverInfo[]> {
if (accounts.length === 0) return []
const employeesBySocialId = await getEmployeesBySocialIds(control, uniqueIds)
const presentEmployeeIds = Object.values(employeesBySocialId)
.map((it) => it?._id)
.filter((it) => it !== undefined)
const missingSocialIds = Object.entries(employeesBySocialId)
.filter(([, employee]) => employee === undefined)
.map(([id]) => id as PersonId)
const personsBySocialId = await getPersonsBySocialIds(control, missingSocialIds)
const employeesIds = new Set(presentEmployeeIds)
const spaces = (await control.findAll(ctx, contact.class.PersonSpace, {})).filter((it) =>
employeesIds.has(it.person as Ref<Employee>)
const employees: Pick<Employee, '_id' | 'personUuid'>[] = await control.findAll(
ctx,
contact.mixin.Employee,
{ personUuid: { $in: accounts }, active: true },
{ projection: { _id: 1, personUuid: 1 } }
)
const spacesByEmployee = groupByArray(spaces, (it) => it.person)
if (employees.length === 0) return []
const persons = [...presentEmployeeIds, ...Object.values(personsBySocialId).map((it) => it._id)]
const spaces: Pick<PersonSpace, '_id' | 'person'>[] = await control.queryFind(
ctx,
contact.class.PersonSpace,
{},
{ projection: { _id: 1, person: 1 } }
)
if (spaces.length === 0) return []
const socialStringsByPersons = await getSocialStringsByPersons(control, persons as Ref<Person>[])
const socialIds: Pick<SocialIdentity, '_id' | 'attachedTo'>[] = await control.findAll(
ctx,
contact.class.SocialIdentity,
{ attachedTo: { $in: employees.map((it) => it._id) } },
{ projection: { _id: 1, attachedTo: 1 } }
)
return new Map(
uniqueIds.map((_id) => {
const employee = employeesBySocialId[_id]
const space = employee !== undefined ? spacesByEmployee.get(employee._id)?.[0] : undefined
const person = employee ?? personsBySocialId[_id]
const socialStrings = socialStringsByPersons[person?._id] ?? []
const employeeByAccount = new Map(employees.map((it) => [it.personUuid, it]))
const spaceByPerson = new Map(spaces.map((it) => [it.person, it]))
const socialIdsByEmployee = groupByArray(socialIds, (it) => it.attachedTo)
return [
_id,
{
_id,
person,
socialStrings,
space: space?._id,
account: employee?.personUuid,
employee
}
]
return accounts
.map((account) => {
const employee = employeeByAccount.get(account)
if (employee === undefined) return undefined
const space = spaceByPerson.get(employee._id)
if (space === undefined) return undefined
const info: ReceiverInfo = {
employee: employee._id,
space: space._id,
account,
socialIds: socialIdsByEmployee.get(employee._id)?.map((it) => it._id) ?? []
}
return info
})
)
.filter(notEmpty)
}
export function toReceiverInfo (hierarchy: Hierarchy, info?: SenderInfo | ReceiverInfo): ReceiverInfo | undefined {
if (info === undefined) return undefined
if (info.person === undefined) return undefined
if (!('space' in info)) return undefined
if (info.space === undefined) return undefined
export async function getSenderInfo (
ctx: MeasureContext,
socialId: PersonId,
control: TriggerControl
): Promise<SenderInfo> {
const controlAccount = control.ctx.contextData.account
let account: AccountUuid | undefined = control.ctx.contextData.socialStringsToUsers.get(socialId)
const isEmployee = hierarchy.hasMixin(info.person, contact.mixin.Employee)
if (!isEmployee) return undefined
if (account == null && controlAccount.socialIds.includes(socialId)) {
account = controlAccount.uuid
}
const employee = hierarchy.as(info.person, contact.mixin.Employee)
if (!employee.active || employee.personUuid == null) return undefined
if (account != null) {
return {
socialId,
person: (await control.findAll(ctx, contact.class.Person, { personUuid: account }))[0]
}
}
const socialIdentity = (
await control.findAll(
control.ctx,
contact.class.SocialIdentity,
{ _id: socialId as SocialIdentityRef },
{ limit: 1, projection: { _id: 1, attachedTo: 1 } }
)
)[0]
if (socialIdentity === undefined) {
return { socialId }
}
return {
_id: info._id,
person: employee,
space: info.space,
socialStrings: info.socialStrings,
account: employee.personUuid,
employee
socialId,
person: (await control.findAll(ctx, contact.class.Person, { _id: socialIdentity.attachedTo }))[0]
}
}

View File

@ -59,7 +59,7 @@ export type TypeMatchFunc = Resource<
(
tx: Tx,
doc: Doc,
person: Person,
person: Ref<Person>,
socialIds: PersonId[],
type: NotificationType,
control: TriggerControl
@ -79,7 +79,7 @@ export interface TypeMatch extends NotificationType {
export type NotificationContentProvider = (
doc: Doc,
tx: TxCUD<Doc>,
target: PersonId,
person: Ref<Person>,
control: TriggerControl
) => Promise<NotificationContent>
@ -91,19 +91,15 @@ export interface NotificationPresenter extends Class<Doc> {
}
export interface ReceiverInfo {
_id: PersonId
person: Person
socialStrings: PersonId[]
space: Ref<PersonSpace>
account: AccountUuid
employee: Employee
employee: Ref<Employee>
socialIds: PersonId[]
space: Ref<PersonSpace>
}
export interface SenderInfo {
_id: PersonId
socialId: PersonId
person?: Person
socialStrings: PersonId[]
}
export type NotificationProviderFunc = (
@ -152,7 +148,6 @@ export default plugin(serverNotificationId, {
PushNotificationsHandler: '' as Resource<TriggerFunc>
},
function: {
IsUserInFieldValueTypeMatch: '' as TypeMatchFunc,
IsUserEmployeeInFieldValueTypeMatch: '' as TypeMatchFunc
}
})

View File

@ -16,7 +16,6 @@
import { DocUpdateMessage } from '@hcengineering/activity'
import core, { Doc, Tx, TxCUD, TxCreateDoc, TxProcessor, TxUpdateDoc, type MeasureContext } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { getPrimarySocialIdsByAccounts } from '@hcengineering/server-contact'
import { getResource, translate } from '@hcengineering/platform'
import request, { Request, RequestStatus } from '@hcengineering/request'
import { pushDocUpdateMessages } from '@hcengineering/server-activity-resources'
@ -25,9 +24,9 @@ import {
getCollaborators,
getNotificationProviderControl,
getNotificationTxes,
getTextPresenter,
getUsersInfo,
toReceiverInfo
getReceiversInfo,
getSenderInfo,
getTextPresenter
} from '@hcengineering/server-notification-resources'
/**
@ -146,35 +145,18 @@ async function getRequestNotificationTx (
const notifyContexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, {
objectId: doc._id
})
const collaboratorsPrimarySocialStringsByAccounts = await getPrimarySocialIdsByAccounts(
control,
Array.from(collaborators)
)
const usersInfo = await getUsersInfo(
control.ctx,
[...Object.values(collaboratorsPrimarySocialStringsByAccounts), tx.modifiedBy],
control
)
const senderInfo = usersInfo.get(tx.modifiedBy) ?? {
_id: tx.modifiedBy,
socialStrings: []
}
const receiverInfos = await getReceiversInfo(ctx, Array.from(collaborators), control)
const senderInfo = await getSenderInfo(ctx, tx.modifiedBy, control)
const notificationControl = await getNotificationProviderControl(ctx, control)
for (const target of collaborators) {
const targetInfo = toReceiverInfo(
control.hierarchy,
usersInfo.get(collaboratorsPrimarySocialStringsByAccounts[target])
)
if (targetInfo === undefined) continue
for (const receiver of receiverInfos) {
const txes = await getNotificationTxes(
ctx,
control,
request,
tx,
targetInfo,
receiver,
senderInfo,
{ isOwn: true, isSpace: false, shouldUpdateTimestamp: true },
notifyContexts,

View File

@ -100,7 +100,7 @@ export async function OnMessageCreate (txes: Tx[], control: TriggerControl): Pro
export function IsIncomingMessageTypeMatch (
tx: Tx,
doc: Doc,
person: Person,
person: Ref<Person>,
user: PersonId[],
type: NotificationType,
control: TriggerControl

View File

@ -14,7 +14,7 @@
//
import { Analytics } from '@hcengineering/analytics'
import contact, { Employee, Person, pickPrimarySocialId } from '@hcengineering/contact'
import contact, { Employee, Person } from '@hcengineering/contact'
import core, {
AttachedData,
@ -37,13 +37,14 @@ import core, {
import notification, { CommonInboxNotification } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import { getSocialStrings, getPerson, getAllSocialStringsByPersonId } from '@hcengineering/server-contact'
import { getSocialStrings } from '@hcengineering/server-contact'
import { ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
import {
getCommonNotificationTxes,
getNotificationContent,
getNotificationProviderControl,
isShouldNotifyTx
isShouldNotifyTx,
getSenderInfo
} from '@hcengineering/server-notification-resources'
import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time'
import task, { makeRank } from '@hcengineering/task'
@ -295,44 +296,33 @@ export async function OnToDoCreate (txes: TxCUD<Doc>[], control: TriggerControl)
continue
}
const socialStrings = await getSocialStrings(control, employee._id)
const primarySocialString = pickPrimarySocialId(socialStrings)
const socialIds = await getSocialStrings(control, employee._id)
const account = employee.personUuid
if (account == null) {
continue
}
// TODO: Select a proper account
const receiverInfo: ReceiverInfo = {
_id: primarySocialString,
person: employee,
socialStrings,
employee,
account,
space: personSpace._id
socialIds,
space: personSpace._id,
employee: employee._id
}
const senderPerson = await getPerson(control, tx.modifiedBy)
const senderSocialStrings = await getAllSocialStringsByPersonId(control, [tx.modifiedBy])
const senderInfo: SenderInfo = {
_id: tx.modifiedBy,
person: senderPerson,
socialStrings: senderSocialStrings
}
const senderInfo: SenderInfo = await getSenderInfo(control.ctx, tx.modifiedBy, control)
const notificationControl = await getNotificationProviderControl(control.ctx, control)
const notifyResult = await isShouldNotifyTx(
control,
createTx,
todo,
socialStrings,
employee._id,
socialIds,
true,
false,
notificationControl
)
const content = await getNotificationContent(tx, socialStrings, senderInfo, todo, control)
const content = await getNotificationContent(tx, employee._id, senderInfo, todo, control)
const data: Partial<Data<CommonInboxNotification>> = {
...content,
header: time.string.ToDo,
@ -361,12 +351,9 @@ export async function OnToDoCreate (txes: TxCUD<Doc>[], control: TriggerControl)
await control.apply(control.ctx, txes)
const ids = txes.map((it) => it._id)
const personUuid = receiverInfo.person?.personUuid
if (personUuid !== undefined) {
control.ctx.contextData.broadcast.targets.notifications = (it) => {
if (ids.includes(it._id)) {
return [personUuid]
}
control.ctx.contextData.broadcast.targets.notifications = (it) => {
if (ids.includes(it._id)) {
return [receiverInfo.account]
}
}
}

View File

@ -20,7 +20,6 @@ import core, {
concatLink,
Doc,
DocumentUpdate,
PersonId,
Ref,
Space,
systemAccountUuid,
@ -34,7 +33,6 @@ import core, {
} from '@hcengineering/core'
import { NotificationContent } from '@hcengineering/notification'
import { getMetadata, IntlString } from '@hcengineering/platform'
import { getSocialStrings } from '@hcengineering/server-contact'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
import { stripTags } from '@hcengineering/text-core'
@ -89,18 +87,13 @@ export async function issueTextPresenter (doc: Doc): Promise<string> {
return `${issue.identifier} ${issue.title}`
}
async function isSamePerson (control: TriggerControl, assignee: Ref<Person>, target: PersonId): Promise<boolean> {
const socialStrings = await getSocialStrings(control, assignee)
return socialStrings.includes(target)
}
/**
* @public
*/
export async function getIssueNotificationContent (
doc: Doc,
tx: TxCUD<Doc>,
target: PersonId,
target: Ref<Person>,
control: TriggerControl
): Promise<NotificationContent> {
const issue = doc as Issue
@ -127,7 +120,7 @@ export async function getIssueNotificationContent (
if (
updateTx.operations.assignee !== null &&
updateTx.operations.assignee !== undefined &&
(await isSamePerson(control, updateTx.operations.assignee, target))
updateTx.operations.assignee === target
) {
body = tracker.string.IssueAssignedToYou
} else {

View File

@ -24,13 +24,13 @@ import { isTxUpdateDoc } from '../utils/isTxUpdateDoc'
export function TrainingRequestNotificationTypeMatch (
tx: TxCUD<TrainingRequest>,
doc: TrainingRequest,
person: Person,
person: Ref<Person>,
user: PersonId[],
type: NotificationType,
control: TriggerControl
): boolean {
if (isTxCreateDoc(tx)) {
return doc.trainees.includes(person._id as Ref<Employee>)
return doc.trainees.includes(person as Ref<Employee>)
}
if (isTxUpdateDoc(tx)) {
@ -40,7 +40,7 @@ export function TrainingRequestNotificationTypeMatch (
}
const newTrainees = typeof pushed === 'object' ? pushed.$each : [pushed]
return newTrainees.includes(person._id as Ref<Employee>)
return newTrainees.includes(person as Ref<Employee>)
}
return false