Do not send unavailable notifications (#7737)
Some checks are pending
CI / uitest (push) Waiting to run
CI / test (push) Blocked by required conditions
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2025-01-20 21:38:36 +04:00 committed by GitHub
parent 7b58315863
commit 3e9442208f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 175 additions and 60 deletions

View File

@ -904,3 +904,18 @@ export class TimeRateLimiter {
} }
} }
} }
export function combineAttributes (
attributes: any[],
key: string,
operator: '$push' | '$pull',
arrayKey: '$each' | '$in'
): any[] {
return Array.from(
new Set(
attributes.flatMap((attr) =>
Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key]
)
)
).filter((v) => v != null)
}

View File

@ -141,6 +141,7 @@ export async function createReactionNotifications (tx: TxCUD<Reaction>, control:
res = res.concat( res = res.concat(
await createCollabDocInfo( await createCollabDocInfo(
control.ctx, control.ctx,
res,
[user] as Ref<PersonAccount>[], [user] as Ref<PersonAccount>[],
control, control,
tx, tx,

View File

@ -15,7 +15,8 @@ import {
TxCUD, TxCUD,
TxMixin, TxMixin,
TxProcessor, TxProcessor,
TxUpdateDoc TxUpdateDoc,
combineAttributes
} from '@hcengineering/core' } from '@hcengineering/core'
import core from '@hcengineering/core/src/component' import core from '@hcengineering/core/src/component'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
@ -76,16 +77,6 @@ function getModifiedAttributes (tx: TxCUD<Doc>, hierarchy: Hierarchy): Record<st
return {} return {}
} }
function combineAttributes (attributes: any[], key: string, operator: string, arrayKey: string): any[] {
return Array.from(
new Set(
attributes.flatMap((attr) =>
Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key]
)
)
).filter((v) => v != null)
}
export function getDocUpdateAction (control: ActivityControl, tx: TxCUD<Doc>): DocUpdateAction { export function getDocUpdateAction (control: ActivityControl, tx: TxCUD<Doc>): DocUpdateAction {
const hierarchy = control.hierarchy const hierarchy = control.hierarchy

View File

@ -34,7 +34,8 @@ import core, {
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc, TxUpdateDoc,
UserStatus, UserStatus,
type MeasureContext type MeasureContext,
combineAttributes
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification' import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification'
import { getMetadata, IntlString, translate } from '@hcengineering/platform' import { getMetadata, IntlString, translate } from '@hcengineering/platform'
@ -359,16 +360,6 @@ async function OnChatMessageRemoved (txes: TxCUD<ChatMessage>[], control: Trigge
return res return res
} }
function combineAttributes (attributes: any[], key: string, operator: string, arrayKey: string): any[] {
return Array.from(
new Set(
attributes.flatMap((attr) =>
Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key]
)
)
).filter((v) => v != null)
}
function getDirectsToHide (directs: DocNotifyContext[], date: Timestamp): DocNotifyContext[] { function getDirectsToHide (directs: DocNotifyContext[], date: Timestamp): DocNotifyContext[] {
const minVisibleDirects = 10 const minVisibleDirects = 10

View File

@ -26,7 +26,8 @@ import core, {
TxMixin, TxMixin,
TxProcessor, TxProcessor,
TxUpdateDoc, TxUpdateDoc,
UserStatus UserStatus,
combineAttributes
} from '@hcengineering/core' } from '@hcengineering/core'
import love, { import love, {
Invite, Invite,
@ -431,16 +432,6 @@ async function isRoomEmpty (
return false return false
} }
function combineAttributes (attributes: any[], key: string, operator: string, arrayKey: string): any[] {
return Array.from(
new Set(
attributes.flatMap((attr) =>
Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key]
)
)
).filter((v) => v != null)
}
async function OnRoomInfo (txes: TxCUD<RoomInfo>[], control: TriggerControl): Promise<Tx[]> { async function OnRoomInfo (txes: TxCUD<RoomInfo>[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = [] const result: Tx[] = []
const personsByRoom = new Map<Ref<RoomInfo>, Ref<Person>[]>() const personsByRoom = new Map<Ref<RoomInfo>, Ref<Person>[]>()

View File

@ -31,6 +31,7 @@ import core, {
AttachedDoc, AttachedDoc,
Class, Class,
Collection, Collection,
combineAttributes,
concatLink, concatLink,
Data, Data,
Doc, Doc,
@ -82,6 +83,7 @@ import { markupToText, stripTags } from '@hcengineering/text'
import { encodeObjectURI } from '@hcengineering/view' import { encodeObjectURI } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import webpush, { WebPushError } from 'web-push' import webpush, { WebPushError } from 'web-push'
import { Analytics } from '@hcengineering/analytics'
import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types' import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types'
import { import {
@ -102,7 +104,8 @@ import {
replaceAll, replaceAll,
toReceiverInfo, toReceiverInfo,
updateNotifyContextsSpace, updateNotifyContextsSpace,
type NotificationProviderControl type NotificationProviderControl,
getObjectSpace
} from './utils' } from './utils'
export function getPushCollaboratorTx ( export function getPushCollaboratorTx (
@ -933,16 +936,16 @@ async function removeContexts (
export async function createCollabDocInfo ( export async function createCollabDocInfo (
ctx: MeasureContext, ctx: MeasureContext,
res: Tx[],
collaborators: Ref<PersonAccount>[], collaborators: Ref<PersonAccount>[],
control: TriggerControl, control: TriggerControl,
tx: TxCUD<Doc>, tx: TxCUD<Doc>,
object: Doc, object: Doc,
activityMessages: ActivityMessage[], activityMessages: ActivityMessage[],
params: NotifyParams, params: NotifyParams,
unsubscribe: Ref<PersonAccount>[] = [] unsubscribe: Ref<PersonAccount>[] = [],
cache = new Map<Ref<Doc>, Doc>()
): Promise<Tx[]> { ): Promise<Tx[]> {
let res: Tx[] = []
if (tx.space === core.space.DerivedTx) { if (tx.space === core.space.DerivedTx) {
return res return res
} }
@ -973,7 +976,35 @@ export async function createCollabDocInfo (
await removeContexts(ctx, notifyContexts, unsubscribe, control) await removeContexts(ctx, notifyContexts, unsubscribe, control)
} }
const targets = new Set(collaborators) const space = await getObjectSpace(control, object, cache)
if (space === undefined) {
control.ctx.error('Cannot find space for object', object)
Analytics.handleError(
new Error(`Cannot find space ${object.space} for objectId ${object._id}, objectClass ${object._class}`)
)
return res
}
cache.set(space._id, space)
const filteredCollaborators = control.hierarchy.isDerived(object._class, core.class.SystemSpace)
? collaborators
: collaborators.filter(
(it) =>
space.members.includes(it) ||
res.some((tx) => {
if (tx._class === core.class.TxUpdateDoc) {
const updateTx = tx as TxUpdateDoc<Space>
if (updateTx.objectId === space._id) {
const added = combineAttributes([updateTx.operations], 'members', '$push', '$each')
return added.includes(it)
}
}
return false
})
)
const targets = new Set(filteredCollaborators)
// user is not collaborator of himself, but we should notify user of changes related to users account (mentions, comments etc) // user is not collaborator of himself, but we should notify user of changes related to users account (mentions, comments etc)
if (control.hierarchy.isDerived(object._class, contact.class.Person)) { if (control.hierarchy.isDerived(object._class, contact.class.Person)) {
@ -1106,8 +1137,7 @@ async function getSpaceCollabTxes (
return [] return []
} }
const space = const space = await getObjectSpace(control, doc, cache)
cache.get(doc.space) ?? (await control.findAll(ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0]
if (space === undefined) return [] if (space === undefined) return []
cache.set(space._id, space) cache.set(space._id, space)
@ -1121,18 +1151,45 @@ async function getSpaceCollabTxes (
if (collabs.collaborators !== undefined) { if (collabs.collaborators !== undefined) {
return await createCollabDocInfo( return await createCollabDocInfo(
ctx, ctx,
[],
collabs.collaborators as Ref<PersonAccount>[], collabs.collaborators as Ref<PersonAccount>[],
control, control,
tx, tx,
doc, doc,
activityMessages, activityMessages,
{ isSpace: true, isOwn: false, shouldUpdateTimestamp: true } { isSpace: true, isOwn: false, shouldUpdateTimestamp: true },
[],
cache
) )
} }
} }
return [] return []
} }
async function pushCollaboratorsToPublicSpace (
control: TriggerControl,
doc: Doc,
collaborators: Ref<Account>[],
cache: Map<Ref<Doc>, Doc>
): Promise<Tx[]> {
const space = await getObjectSpace(control, doc, cache)
if (space === undefined) return []
cache.set(space._id, space)
if (control.hierarchy.isDerived(space._class, core.class.SystemSpace)) {
return []
}
if (space.private) {
return []
}
return collaborators
.filter((it) => !space.members.includes(it))
.map((it) => control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { members: it } }))
}
async function createCollaboratorDoc ( async function createCollaboratorDoc (
ctx: MeasureContext, ctx: MeasureContext,
tx: TxCreateDoc<Doc>, tx: TxCreateDoc<Doc>,
@ -1152,15 +1209,7 @@ async function createCollaboratorDoc (
const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getDocCollaborators(ctx, doc, mixin, control)) const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getDocCollaborators(ctx, doc, mixin, control))
const mixinTx = getMixinTx(tx, control, collaborators) const mixinTx = getMixinTx(tx, control, collaborators)
const notificationTxes = await ctx.with('create-collabdocinfo', {}, (ctx) =>
createCollabDocInfo(ctx, collaborators as Ref<PersonAccount>[], control, tx, doc, activityMessage, {
isOwn: true,
isSpace: false,
shouldUpdateTimestamp: true
})
)
res.push(mixinTx) res.push(mixinTx)
res.push(...notificationTxes)
res.push( res.push(
...(await ctx.with('get-space-collabtxes', {}, (ctx) => ...(await ctx.with('get-space-collabtxes', {}, (ctx) =>
@ -1168,6 +1217,29 @@ async function createCollaboratorDoc (
)) ))
) )
res.push(...(await pushCollaboratorsToPublicSpace(control, doc, collaborators, cache)))
const notificationTxes = await ctx.with('create-collabdocinfo', {}, (ctx) =>
createCollabDocInfo(
ctx,
res,
collaborators as Ref<PersonAccount>[],
control,
tx,
doc,
activityMessage,
{
isOwn: true,
isSpace: false,
shouldUpdateTimestamp: true
},
[],
cache
)
)
res.push(...notificationTxes)
return res return res
} }
@ -1176,7 +1248,8 @@ async function updateCollaboratorsMixin (
tx: TxMixin<Doc, Collaborators>, tx: TxMixin<Doc, Collaborators>,
control: TriggerControl, control: TriggerControl,
activityMessages: ActivityMessage[], activityMessages: ActivityMessage[],
originTx: TxCUD<Doc> originTx: TxCUD<Doc>,
cache: Map<Ref<Doc>, Doc>
): Promise<Tx[]> { ): Promise<Tx[]> {
const { hierarchy } = control const { hierarchy } = control
@ -1232,6 +1305,13 @@ async function updateCollaboratorsMixin (
} }
if (newCollabs.length > 0) { if (newCollabs.length > 0) {
const object = cache.get(tx.objectId) ?? (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }))[0]
if (object === undefined) return res
const space = await getObjectSpace(control, object, cache)
cache.set(object._id, object)
cache.set(space._id, space)
const docNotifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, { const docNotifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, {
user: { $in: newCollabs }, user: { $in: newCollabs },
objectId: tx.objectId objectId: tx.objectId
@ -1245,6 +1325,15 @@ async function updateCollaboratorsMixin (
for (const collab of newCollabs) { for (const collab of newCollabs) {
const target = toReceiverInfo(hierarchy, infos.get(collab)) const target = toReceiverInfo(hierarchy, infos.get(collab))
if (target === undefined) continue if (target === undefined) continue
if (space.private && !space.members.includes(target.account._id)) continue
if (!hierarchy.isDerived(space._class, core.class.SystemSpace) && !space.members.includes(target.account._id)) {
res.push(
control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
$push: { members: target.account._id }
})
)
}
for (const message of activityMessages) { for (const message of activityMessages) {
await pushActivityInboxNotifications( await pushActivityInboxNotifications(
@ -1309,11 +1398,22 @@ async function collectionCollabDoc (
res = res.concat( res = res.concat(
await ctx.with('create-collab-doc-info', {}, (ctx) => await ctx.with('create-collab-doc-info', {}, (ctx) =>
createCollabDocInfo(ctx, collaborators as Ref<PersonAccount>[], control, tx, doc, activityMessages, { createCollabDocInfo(
isOwn: false, ctx,
isSpace: false, res,
shouldUpdateTimestamp: true collaborators as Ref<PersonAccount>[],
}) control,
tx,
doc,
activityMessages,
{
isOwn: false,
isSpace: false,
shouldUpdateTimestamp: true
},
[],
cache
)
) )
) )
@ -1446,7 +1546,6 @@ async function updateCollaboratorDoc (
ctx: MeasureContext, ctx: MeasureContext,
tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>, tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>,
control: TriggerControl, control: TriggerControl,
originTx: TxCUD<Doc>,
activityMessages: ActivityMessage[], activityMessages: ActivityMessage[],
cache: Map<Ref<Doc>, Doc> cache: Map<Ref<Doc>, Doc>
): Promise<Tx[]> { ): Promise<Tx[]> {
@ -1479,13 +1578,15 @@ async function updateCollaboratorDoc (
await ctx.with('create-collab-docinfo', {}, (ctx) => await ctx.with('create-collab-docinfo', {}, (ctx) =>
createCollabDocInfo( createCollabDocInfo(
ctx, ctx,
res,
collabsInfo.result as Ref<PersonAccount>[], collabsInfo.result as Ref<PersonAccount>[],
control, control,
tx, tx,
doc, doc,
activityMessages, activityMessages,
params, params,
collabsInfo.removed as Ref<PersonAccount>[] collabsInfo.removed as Ref<PersonAccount>[],
cache
) )
) )
) )
@ -1495,7 +1596,18 @@ async function updateCollaboratorDoc (
) )
res.push(getMixinTx(tx, control, collaborators)) res.push(getMixinTx(tx, control, collaborators))
res = res.concat( res = res.concat(
await createCollabDocInfo(ctx, collaborators as Ref<PersonAccount>[], control, tx, doc, activityMessages, params) await createCollabDocInfo(
ctx,
res,
collaborators as Ref<PersonAccount>[],
control,
tx,
doc,
activityMessages,
params,
[],
cache
)
) )
} }
@ -1737,11 +1849,18 @@ export async function createCollaboratorNotifications (
case core.class.TxUpdateDoc: case core.class.TxUpdateDoc:
case core.class.TxMixin: { case core.class.TxMixin: {
let res = await ctx.with('updateCollaboratorDoc', {}, (ctx) => let res = await ctx.with('updateCollaboratorDoc', {}, (ctx) =>
updateCollaboratorDoc(ctx, tx as TxUpdateDoc<Doc>, control, originTx ?? tx, activityMessages, cache) updateCollaboratorDoc(ctx, tx as TxUpdateDoc<Doc>, control, activityMessages, cache)
) )
res = res.concat( res = res.concat(
await ctx.with('updateCollaboratorMixin', {}, (ctx) => await ctx.with('updateCollaboratorMixin', {}, (ctx) =>
updateCollaboratorsMixin(ctx, tx as TxMixin<Doc, Collaborators>, control, activityMessages, originTx ?? tx) updateCollaboratorsMixin(
ctx,
tx as TxMixin<Doc, Collaborators>,
control,
activityMessages,
originTx ?? tx,
cache
)
) )
) )
return await applyUserTxes(ctx, control, res) return await applyUserTxes(ctx, control, res)

View File

@ -654,3 +654,10 @@ export async function getNotificationProviderControl (
} }
return new NotificationProviderControl(providersSettings, typesSettings) return new NotificationProviderControl(providersSettings, typesSettings)
} }
export async function getObjectSpace (control: TriggerControl, doc: Doc, cache: Map<Ref<Doc>, Doc>): Promise<Space> {
return control.hierarchy.isDerived(doc._class, core.class.Space)
? (doc as Space)
: (cache.get(doc.space) as Space) ??
(await control.findAll<Space>(control.ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0]
}