diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 61289073fc..45e5a8b943 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -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) +} diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts index 71454aac57..4a59ffd799 100644 --- a/server-plugins/activity-resources/src/index.ts +++ b/server-plugins/activity-resources/src/index.ts @@ -141,6 +141,7 @@ export async function createReactionNotifications (tx: TxCUD, control: res = res.concat( await createCollabDocInfo( control.ctx, + res, [user] as Ref[], control, tx, diff --git a/server-plugins/activity-resources/src/utils.ts b/server-plugins/activity-resources/src/utils.ts index e2d020ba96..966c633eca 100644 --- a/server-plugins/activity-resources/src/utils.ts +++ b/server-plugins/activity-resources/src/utils.ts @@ -15,7 +15,8 @@ import { TxCUD, TxMixin, TxProcessor, - TxUpdateDoc + TxUpdateDoc, + combineAttributes } from '@hcengineering/core' import core from '@hcengineering/core/src/component' import notification from '@hcengineering/notification' @@ -76,16 +77,6 @@ function getModifiedAttributes (tx: TxCUD, hierarchy: Hierarchy): Record - Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key] - ) - ) - ).filter((v) => v != null) -} - export function getDocUpdateAction (control: ActivityControl, tx: TxCUD): DocUpdateAction { const hierarchy = control.hierarchy diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 7a0ff58d9c..bfff959b81 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -34,7 +34,8 @@ import core, { TxRemoveDoc, TxUpdateDoc, UserStatus, - type MeasureContext + type MeasureContext, + combineAttributes } from '@hcengineering/core' import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification' import { getMetadata, IntlString, translate } from '@hcengineering/platform' @@ -359,16 +360,6 @@ async function OnChatMessageRemoved (txes: TxCUD[], control: Trigge 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[] { const minVisibleDirects = 10 diff --git a/server-plugins/love-resources/src/index.ts b/server-plugins/love-resources/src/index.ts index d554834b64..a5677b9774 100644 --- a/server-plugins/love-resources/src/index.ts +++ b/server-plugins/love-resources/src/index.ts @@ -26,7 +26,8 @@ import core, { TxMixin, TxProcessor, TxUpdateDoc, - UserStatus + UserStatus, + combineAttributes } from '@hcengineering/core' import love, { Invite, @@ -431,16 +432,6 @@ async function isRoomEmpty ( 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[], control: TriggerControl): Promise { const result: Tx[] = [] const personsByRoom = new Map, Ref[]>() diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index b93d851295..2824c4ff12 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -31,6 +31,7 @@ import core, { AttachedDoc, Class, Collection, + combineAttributes, concatLink, Data, Doc, @@ -82,6 +83,7 @@ import { markupToText, stripTags } from '@hcengineering/text' import { encodeObjectURI } from '@hcengineering/view' import { workbenchId } from '@hcengineering/workbench' import webpush, { WebPushError } from 'web-push' +import { Analytics } from '@hcengineering/analytics' import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types' import { @@ -102,7 +104,8 @@ import { replaceAll, toReceiverInfo, updateNotifyContextsSpace, - type NotificationProviderControl + type NotificationProviderControl, + getObjectSpace } from './utils' export function getPushCollaboratorTx ( @@ -933,16 +936,16 @@ async function removeContexts ( export async function createCollabDocInfo ( ctx: MeasureContext, + res: Tx[], collaborators: Ref[], control: TriggerControl, tx: TxCUD, object: Doc, activityMessages: ActivityMessage[], params: NotifyParams, - unsubscribe: Ref[] = [] + unsubscribe: Ref[] = [], + cache = new Map, Doc>() ): Promise { - let res: Tx[] = [] - if (tx.space === core.space.DerivedTx) { return res } @@ -973,7 +976,35 @@ export async function createCollabDocInfo ( 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 + 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) if (control.hierarchy.isDerived(object._class, contact.class.Person)) { @@ -1106,8 +1137,7 @@ async function getSpaceCollabTxes ( return [] } - const space = - cache.get(doc.space) ?? (await control.findAll(ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0] + const space = await getObjectSpace(control, doc, cache) if (space === undefined) return [] cache.set(space._id, space) @@ -1121,18 +1151,45 @@ async function getSpaceCollabTxes ( if (collabs.collaborators !== undefined) { return await createCollabDocInfo( ctx, + [], collabs.collaborators as Ref[], control, tx, doc, activityMessages, - { isSpace: true, isOwn: false, shouldUpdateTimestamp: true } + { isSpace: true, isOwn: false, shouldUpdateTimestamp: true }, + [], + cache ) } } return [] } +async function pushCollaboratorsToPublicSpace ( + control: TriggerControl, + doc: Doc, + collaborators: Ref[], + cache: Map, Doc> +): Promise { + 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 ( ctx: MeasureContext, tx: TxCreateDoc, @@ -1152,15 +1209,7 @@ async function createCollaboratorDoc ( const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getDocCollaborators(ctx, doc, mixin, control)) const mixinTx = getMixinTx(tx, control, collaborators) - const notificationTxes = await ctx.with('create-collabdocinfo', {}, (ctx) => - createCollabDocInfo(ctx, collaborators as Ref[], control, tx, doc, activityMessage, { - isOwn: true, - isSpace: false, - shouldUpdateTimestamp: true - }) - ) res.push(mixinTx) - res.push(...notificationTxes) res.push( ...(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[], + control, + tx, + doc, + activityMessage, + { + isOwn: true, + isSpace: false, + shouldUpdateTimestamp: true + }, + [], + cache + ) + ) + + res.push(...notificationTxes) + return res } @@ -1176,7 +1248,8 @@ async function updateCollaboratorsMixin ( tx: TxMixin, control: TriggerControl, activityMessages: ActivityMessage[], - originTx: TxCUD + originTx: TxCUD, + cache: Map, Doc> ): Promise { const { hierarchy } = control @@ -1232,6 +1305,13 @@ async function updateCollaboratorsMixin ( } 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, { user: { $in: newCollabs }, objectId: tx.objectId @@ -1245,6 +1325,15 @@ async function updateCollaboratorsMixin ( for (const collab of newCollabs) { const target = toReceiverInfo(hierarchy, infos.get(collab)) 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) { await pushActivityInboxNotifications( @@ -1309,11 +1398,22 @@ async function collectionCollabDoc ( res = res.concat( await ctx.with('create-collab-doc-info', {}, (ctx) => - createCollabDocInfo(ctx, collaborators as Ref[], control, tx, doc, activityMessages, { - isOwn: false, - isSpace: false, - shouldUpdateTimestamp: true - }) + createCollabDocInfo( + ctx, + res, + collaborators as Ref[], + control, + tx, + doc, + activityMessages, + { + isOwn: false, + isSpace: false, + shouldUpdateTimestamp: true + }, + [], + cache + ) ) ) @@ -1446,7 +1546,6 @@ async function updateCollaboratorDoc ( ctx: MeasureContext, tx: TxUpdateDoc | TxMixin, control: TriggerControl, - originTx: TxCUD, activityMessages: ActivityMessage[], cache: Map, Doc> ): Promise { @@ -1479,13 +1578,15 @@ async function updateCollaboratorDoc ( await ctx.with('create-collab-docinfo', {}, (ctx) => createCollabDocInfo( ctx, + res, collabsInfo.result as Ref[], control, tx, doc, activityMessages, params, - collabsInfo.removed as Ref[] + collabsInfo.removed as Ref[], + cache ) ) ) @@ -1495,7 +1596,18 @@ async function updateCollaboratorDoc ( ) res.push(getMixinTx(tx, control, collaborators)) res = res.concat( - await createCollabDocInfo(ctx, collaborators as Ref[], control, tx, doc, activityMessages, params) + await createCollabDocInfo( + ctx, + res, + collaborators as Ref[], + control, + tx, + doc, + activityMessages, + params, + [], + cache + ) ) } @@ -1737,11 +1849,18 @@ export async function createCollaboratorNotifications ( case core.class.TxUpdateDoc: case core.class.TxMixin: { let res = await ctx.with('updateCollaboratorDoc', {}, (ctx) => - updateCollaboratorDoc(ctx, tx as TxUpdateDoc, control, originTx ?? tx, activityMessages, cache) + updateCollaboratorDoc(ctx, tx as TxUpdateDoc, control, activityMessages, cache) ) res = res.concat( await ctx.with('updateCollaboratorMixin', {}, (ctx) => - updateCollaboratorsMixin(ctx, tx as TxMixin, control, activityMessages, originTx ?? tx) + updateCollaboratorsMixin( + ctx, + tx as TxMixin, + control, + activityMessages, + originTx ?? tx, + cache + ) ) ) return await applyUserTxes(ctx, control, res) diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index e95a5733a0..271692f478 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -654,3 +654,10 @@ export async function getNotificationProviderControl ( } return new NotificationProviderControl(providersSettings, typesSettings) } + +export async function getObjectSpace (control: TriggerControl, doc: Doc, cache: Map, Doc>): Promise { + return control.hierarchy.isDerived(doc._class, core.class.Space) + ? (doc as Space) + : (cache.get(doc.space) as Space) ?? + (await control.findAll(control.ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0] +}