diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts
index 4156981bf9..dfd25f4c10 100644
--- a/models/chunter/src/index.ts
+++ b/models/chunter/src/index.ts
@@ -13,7 +13,7 @@
// limitations under the License.
//
-import activity, { type ActivityMessage } from '@hcengineering/activity'
+import activity, { type ActivityMessage, type ActivityMessageControl } from '@hcengineering/activity'
import {
type Channel,
chunterId,
@@ -62,6 +62,7 @@ import { type DocNotifyContext } from '@hcengineering/notification'
import chunter from './plugin'
import { defineActions } from './actions'
+import { defineNotifications } from './notifications'
export { chunterId } from '@hcengineering/chunter'
export { chunterOperation } from './migration'
@@ -197,37 +198,14 @@ export function createModel (builder: Builder): void {
titleProvider: chunter.function.ChannelTitleProvider
})
- builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.ClassCollaborators, {
- fields: ['members']
- })
-
- builder.mixin(chunter.class.Channel, core.class.Class, notification.mixin.ClassCollaborators, {
- fields: ['members']
- })
-
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectPresenter, {
presenter: chunter.component.DmPresenter
})
- builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.NotificationPreview, {
- presenter: chunter.component.ChannelPreview
- })
-
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectPresenter, {
presenter: chunter.component.ChannelPresenter
})
- builder.mixin(chunter.class.ChatMessage, core.class.Class, notification.mixin.NotificationContextPresenter, {
- labelPresenter: chunter.component.ChatMessageNotificationLabel
- })
-
- builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
- messageMatch: {
- _class: chunter.class.ThreadMessage
- },
- presenter: chunter.component.ThreadNotificationPresenter
- })
-
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.SpaceHeader, {
header: chunter.component.DmHeader
})
@@ -284,78 +262,6 @@ export function createModel (builder: Builder): void {
filters: []
})
- builder.createDoc(
- notification.class.NotificationGroup,
- core.space.Model,
- {
- label: chunter.string.ApplicationLabelChunter,
- icon: chunter.icon.Chunter
- },
- chunter.ids.ChunterNotificationGroup
- )
-
- builder.createDoc(
- notification.class.NotificationType,
- core.space.Model,
- {
- label: chunter.string.DM,
- generated: false,
- hidden: false,
- txClasses: [core.class.TxCreateDoc],
- objectClass: chunter.class.ChatMessage,
- attachedToClass: chunter.class.DirectMessage,
- defaultEnabled: false,
- group: chunter.ids.ChunterNotificationGroup,
- templates: {
- textTemplate: '{sender} has sent you a message: {doc} {message}',
- htmlTemplate: '
{sender} has sent you a message {doc}
{message}',
- subjectTemplate: 'You have new direct message in {doc}'
- }
- },
- chunter.ids.DMNotification
- )
-
- builder.createDoc(
- notification.class.NotificationType,
- core.space.Model,
- {
- label: chunter.string.Message,
- generated: false,
- hidden: false,
- txClasses: [core.class.TxCreateDoc],
- objectClass: chunter.class.ChatMessage,
- attachedToClass: chunter.class.Channel,
- defaultEnabled: false,
- group: chunter.ids.ChunterNotificationGroup,
- templates: {
- textTemplate: '{sender} has sent a message in {doc}: {message}',
- htmlTemplate: '{sender} has sent a message in {doc}
{message}',
- subjectTemplate: 'You have new message in {doc}'
- }
- },
- chunter.ids.ChannelNotification
- )
-
- builder.createDoc(
- notification.class.NotificationType,
- core.space.Model,
- {
- label: chunter.string.ThreadMessage,
- generated: false,
- hidden: false,
- txClasses: [core.class.TxCreateDoc],
- objectClass: chunter.class.ThreadMessage,
- defaultEnabled: false,
- group: chunter.ids.ChunterNotificationGroup,
- templates: {
- textTemplate: '{body}',
- htmlTemplate: '{body}
',
- subjectTemplate: '{title}'
- }
- },
- chunter.ids.ThreadNotification
- )
-
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
label: chunter.string.Comments,
position: 60,
@@ -419,11 +325,11 @@ export function createModel (builder: Builder): void {
})
builder.mixin(chunter.class.Channel, core.class.Class, chunter.mixin.ObjectChatPanel, {
- ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description']
+ ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description', 'members', 'owners']
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, chunter.mixin.ObjectChatPanel, {
- ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description']
+ ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description', 'members', 'owners']
})
builder.mixin(chunter.class.ChatMessage, core.class.Class, activity.mixin.ActivityMessagePreview, {
@@ -448,19 +354,50 @@ export function createModel (builder: Builder): void {
strict: true
})
- builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
- provider: notification.providers.InboxNotificationProvider,
- ignoredTypes: [],
- enabledTypes: [chunter.ids.DMNotification, chunter.ids.ChannelNotification, chunter.ids.ThreadNotification]
+ builder.createDoc>(activity.class.ActivityMessageControl, core.space.Model, {
+ objectClass: chunter.class.Channel,
+ skip: [
+ { _class: core.class.TxMixin },
+ { _class: core.class.TxCreateDoc, objectClass: { $ne: chunter.class.Channel } },
+ { _class: core.class.TxRemoveDoc }
+ ],
+ allowedFields: ['members']
})
- builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
- provider: notification.providers.PushNotificationProvider,
- ignoredTypes: [],
- enabledTypes: [chunter.ids.DMNotification, chunter.ids.ChannelNotification, chunter.ids.ThreadNotification]
+ builder.createDoc>(activity.class.ActivityMessageControl, core.space.Model, {
+ objectClass: chunter.class.DirectMessage,
+ skip: [{ _class: core.class.TxMixin }, { _class: core.class.TxCreateDoc }, { _class: core.class.TxRemoveDoc }],
+ allowedFields: ['members']
+ })
+
+ builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, {
+ objectClass: chunter.class.Channel,
+ action: 'create',
+ component: chunter.activity.ChannelCreatedMessage
+ })
+
+ builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, {
+ objectClass: chunter.class.Channel,
+ action: 'update',
+ config: {
+ members: {
+ presenter: chunter.activity.MembersChangedMessage
+ }
+ }
+ })
+
+ builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, {
+ objectClass: chunter.class.DirectMessage,
+ action: 'update',
+ config: {
+ members: {
+ presenter: chunter.activity.MembersChangedMessage
+ }
+ }
})
defineActions(builder)
+ defineNotifications(builder)
}
export default chunter
diff --git a/models/chunter/src/migration.ts b/models/chunter/src/migration.ts
index ef1e2b4725..7faa157a29 100644
--- a/models/chunter/src/migration.ts
+++ b/models/chunter/src/migration.ts
@@ -38,6 +38,7 @@ import { DOMAIN_NOTIFICATION } from '@hcengineering/model-notification'
import chunter from './plugin'
import { DOMAIN_CHUNTER } from './index'
+import { type DocUpdateMessage } from '@hcengineering/activity'
export const DOMAIN_COMMENT = 'comment' as Domain
@@ -197,6 +198,47 @@ async function removeOldClasses (client: MigrationClient): Promise {
}
}
+async function removeWrongActivity (client: MigrationClient): Promise {
+ await client.deleteMany(DOMAIN_ACTIVITY, {
+ _class: activity.class.DocUpdateMessage,
+ attachedToClass: chunter.class.Channel,
+ action: 'update',
+ 'attributeUpdates.attrKey': { $ne: 'members' }
+ })
+
+ await client.deleteMany(DOMAIN_ACTIVITY, {
+ _class: activity.class.DocUpdateMessage,
+ attachedToClass: chunter.class.Channel,
+ action: 'create',
+ objectClass: { $ne: chunter.class.Channel }
+ })
+
+ await client.deleteMany(DOMAIN_ACTIVITY, {
+ _class: activity.class.DocUpdateMessage,
+ attachedToClass: chunter.class.Channel,
+ action: 'remove'
+ })
+
+ await client.deleteMany(DOMAIN_ACTIVITY, {
+ _class: activity.class.DocUpdateMessage,
+ attachedToClass: chunter.class.DirectMessage,
+ action: 'update',
+ 'attributeUpdates.attrKey': { $ne: 'members' }
+ })
+
+ await client.deleteMany(DOMAIN_ACTIVITY, {
+ _class: activity.class.DocUpdateMessage,
+ attachedToClass: chunter.class.DirectMessage,
+ action: 'create'
+ })
+
+ await client.deleteMany(DOMAIN_ACTIVITY, {
+ _class: activity.class.DocUpdateMessage,
+ attachedToClass: chunter.class.DirectMessage,
+ action: 'remove'
+ })
+}
+
export const chunterOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise {
await tryMigrate(client, chunterId, [
@@ -235,6 +277,12 @@ export const chunterOperation: MigrateOperation = {
func: async (client) => {
await removeOldClasses(client)
}
+ },
+ {
+ state: 'remove-wrong-activity-v1',
+ func: async (client) => {
+ await removeWrongActivity(client)
+ }
}
])
},
diff --git a/models/chunter/src/notifications.ts b/models/chunter/src/notifications.ts
new file mode 100644
index 0000000000..2c4cd4bd05
--- /dev/null
+++ b/models/chunter/src/notifications.ts
@@ -0,0 +1,171 @@
+//
+// Copyright © 2024 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 { type Builder } from '@hcengineering/model'
+import notification from '@hcengineering/model-notification'
+import core from '@hcengineering/model-core'
+import activity from '@hcengineering/activity'
+
+import chunter from './plugin'
+
+export function defineNotifications (builder: Builder): void {
+ builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.ClassCollaborators, {
+ fields: ['members']
+ })
+
+ builder.mixin(chunter.class.Channel, core.class.Class, notification.mixin.ClassCollaborators, {
+ fields: ['members']
+ })
+
+ builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.NotificationPreview, {
+ presenter: chunter.component.ChannelPreview
+ })
+
+ builder.mixin(chunter.class.ChatMessage, core.class.Class, notification.mixin.NotificationContextPresenter, {
+ labelPresenter: chunter.component.ChatMessageNotificationLabel
+ })
+
+ builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
+ messageMatch: {
+ _class: chunter.class.ThreadMessage
+ },
+ presenter: chunter.component.ThreadNotificationPresenter
+ })
+
+ builder.createDoc(
+ notification.class.NotificationGroup,
+ core.space.Model,
+ {
+ label: chunter.string.ApplicationLabelChunter,
+ icon: chunter.icon.Chunter
+ },
+ chunter.ids.ChunterNotificationGroup
+ )
+
+ builder.createDoc(
+ notification.class.NotificationType,
+ core.space.Model,
+ {
+ label: chunter.string.DM,
+ generated: false,
+ hidden: false,
+ txClasses: [core.class.TxCreateDoc],
+ objectClass: chunter.class.ChatMessage,
+ attachedToClass: chunter.class.DirectMessage,
+ defaultEnabled: false,
+ group: chunter.ids.ChunterNotificationGroup,
+ templates: {
+ textTemplate: '{sender} has sent you a message: {doc} {message}',
+ htmlTemplate: '{sender} has sent you a message {doc}
{message}',
+ subjectTemplate: 'You have new direct message in {doc}'
+ }
+ },
+ chunter.ids.DMNotification
+ )
+
+ builder.createDoc(
+ notification.class.NotificationType,
+ core.space.Model,
+ {
+ label: chunter.string.ChannelMessages,
+ generated: false,
+ hidden: false,
+ txClasses: [core.class.TxCreateDoc],
+ objectClass: chunter.class.ChatMessage,
+ attachedToClass: chunter.class.Channel,
+ defaultEnabled: false,
+ group: chunter.ids.ChunterNotificationGroup,
+ templates: {
+ textTemplate: '{sender} has sent a message in {doc}: {message}',
+ htmlTemplate: '{sender} has sent a message in {doc}
{message}',
+ subjectTemplate: 'You have new message in {doc}'
+ }
+ },
+ chunter.ids.ChannelNotification
+ )
+
+ builder.createDoc(
+ notification.class.NotificationType,
+ core.space.Model,
+ {
+ label: chunter.string.JoinChannel,
+ generated: false,
+ hidden: false,
+ txClasses: [core.class.TxUpdateDoc],
+ objectClass: chunter.class.Channel,
+ defaultEnabled: false,
+ field: 'members',
+ group: chunter.ids.ChunterNotificationGroup,
+ templates: {
+ textTemplate: 'You have been added to #{doc}',
+ htmlTemplate: 'You have been added to #{doc}
',
+ subjectTemplate: 'You have been added to #{doc}'
+ }
+ },
+ chunter.ids.JoinChannelNotification
+ )
+
+ builder.createDoc(
+ notification.class.NotificationType,
+ core.space.Model,
+ {
+ label: chunter.string.ThreadMessage,
+ generated: false,
+ hidden: false,
+ txClasses: [core.class.TxCreateDoc],
+ objectClass: chunter.class.ThreadMessage,
+ defaultEnabled: false,
+ group: chunter.ids.ChunterNotificationGroup,
+ templates: {
+ textTemplate: '{body}',
+ htmlTemplate: '{body}
',
+ subjectTemplate: '{title}'
+ }
+ },
+ chunter.ids.ThreadNotification
+ )
+
+ builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
+ provider: notification.providers.InboxNotificationProvider,
+ ignoredTypes: [],
+ enabledTypes: [
+ chunter.ids.DMNotification,
+ chunter.ids.ChannelNotification,
+ chunter.ids.ThreadNotification,
+ chunter.ids.JoinChannelNotification
+ ]
+ })
+
+ builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
+ provider: notification.providers.PushNotificationProvider,
+ ignoredTypes: [],
+ enabledTypes: [
+ chunter.ids.DMNotification,
+ chunter.ids.ChannelNotification,
+ chunter.ids.ThreadNotification,
+ chunter.ids.JoinChannelNotification
+ ]
+ })
+
+ builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
+ messageMatch: {
+ _class: activity.class.DocUpdateMessage,
+ objectClass: chunter.class.Channel,
+ action: 'update',
+ 'attributeUpdates.attrKey': 'members'
+ },
+ presenter: chunter.component.JoinChannelNotificationPresenter
+ })
+}
diff --git a/models/chunter/src/plugin.ts b/models/chunter/src/plugin.ts
index 6c5f131bda..0e039bbd7c 100644
--- a/models/chunter/src/plugin.ts
+++ b/models/chunter/src/plugin.ts
@@ -30,7 +30,8 @@ export default mergeIds(chunterId, chunter, {
ChannelsPanel: '' as AnyComponent,
Chat: '' as AnyComponent,
ChatMessageNotificationLabel: '' as AnyComponent,
- ThreadNotificationPresenter: '' as AnyComponent
+ ThreadNotificationPresenter: '' as AnyComponent,
+ JoinChannelNotificationPresenter: '' as AnyComponent
},
action: {
MarkCommentUnread: '' as Ref,
@@ -51,6 +52,10 @@ export default mergeIds(chunterId, chunter, {
category: {
Chunter: '' as Ref
},
+ activity: {
+ ChannelCreatedMessage: '' as AnyComponent,
+ MembersChangedMessage: '' as AnyComponent
+ },
string: {
ApplicationLabelChunter: '' as IntlString,
MentionedIn: '' as IntlString,
@@ -71,7 +76,9 @@ export default mergeIds(chunterId, chunter, {
ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString,
Reacted: '' as IntlString,
- RepliedToThread: '' as IntlString
+ RepliedToThread: '' as IntlString,
+ ChannelMessages: '' as IntlString,
+ JoinChannel: '' as IntlString
},
viewlet: {
Chat: '' as Ref,
diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts
index b4f6b18f28..d92da09886 100644
--- a/models/contact/src/index.ts
+++ b/models/contact/src/index.ts
@@ -254,16 +254,6 @@ export function createModel (builder: Builder): void {
builder.mixin(contact.class.Organization, core.class.Class, activity.mixin.ActivityDoc, {})
- builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, {
- objectClass: contact.class.Contact,
- skip: [
- {
- _class: core.class.TxCollectionCUD,
- collection: 'comments'
- }
- ]
- })
-
builder.mixin(contact.class.Channel, core.class.Class, activity.mixin.ActivityDoc, {})
builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectIcon, {
diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts
index 7e9e6ed71d..63015a8001 100644
--- a/models/lead/src/index.ts
+++ b/models/lead/src/index.ts
@@ -47,16 +47,6 @@ export function createModel (builder: Builder): void {
builder.mixin(lead.mixin.Customer, core.class.Class, activity.mixin.ActivityDoc, {})
- builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, {
- objectClass: lead.class.Lead,
- skip: [
- {
- _class: core.class.TxCollectionCUD,
- collection: 'comments'
- }
- ]
- })
-
builder.mixin(lead.class.Funnel, core.class.Class, activity.mixin.ActivityDoc, {})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts
index bce4f9a7b7..f9604d7408 100644
--- a/models/recruit/src/index.ts
+++ b/models/recruit/src/index.ts
@@ -54,26 +54,6 @@ export function createModel (builder: Builder): void {
builder.mixin(recruit.class.Review, core.class.Class, activity.mixin.ActivityDoc, {})
builder.mixin(recruit.mixin.Candidate, core.class.Class, activity.mixin.ActivityDoc, {})
- builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, {
- objectClass: recruit.class.Vacancy,
- skip: [
- {
- _class: core.class.TxCollectionCUD,
- collection: 'comments'
- }
- ]
- })
-
- builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, {
- objectClass: recruit.class.Applicant,
- skip: [
- {
- _class: core.class.TxCollectionCUD,
- collection: 'comments'
- }
- ]
- })
-
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: recruit.class.Vacancy,
components: { input: chunter.component.ChatMessageInput }
diff --git a/models/server-activity/src/index.ts b/models/server-activity/src/index.ts
index d0859ae5ef..9a88c922e1 100644
--- a/models/server-activity/src/index.ts
+++ b/models/server-activity/src/index.ts
@@ -42,7 +42,8 @@ export function createModel (builder: Builder): void {
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
- trigger: serverActivity.trigger.ActivityMessagesHandler
+ trigger: serverActivity.trigger.ActivityMessagesHandler,
+ isAsync: true
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
diff --git a/models/server-chunter/src/index.ts b/models/server-chunter/src/index.ts
index cc93beffad..9302dfbf15 100644
--- a/models/server-chunter/src/index.ts
+++ b/models/server-chunter/src/index.ts
@@ -54,14 +54,6 @@ export function createModel (builder: Builder): void {
trigger: serverChunter.trigger.ChunterTrigger
})
- builder.createDoc(serverCore.class.Trigger, core.space.Model, {
- trigger: serverChunter.trigger.OnChannelMembersChanged,
- txMatch: {
- _class: core.class.TxUpdateDoc,
- objectClass: chunter.class.Channel
- }
- })
-
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.OnUserStatus,
txMatch: {
@@ -70,6 +62,15 @@ export function createModel (builder: Builder): void {
isAsync: true
})
+ builder.mixin(
+ chunter.ids.JoinChannelNotification,
+ notification.class.NotificationType,
+ serverNotification.mixin.TypeMatch,
+ {
+ func: serverChunter.function.JoinChannelTypeMatch
+ }
+ )
+
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.OnContextUpdate,
txMatch: {
diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts
index b41eb03e95..cf5e11bc30 100644
--- a/models/tracker/src/index.ts
+++ b/models/tracker/src/index.ts
@@ -449,46 +449,6 @@ export function createModel (builder: Builder): void {
decode: tracker.function.GetIssueIdByIdentifier
})
- builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, {
- objectClass: tracker.class.Issue,
- skip: [
- {
- _class: core.class.TxCollectionCUD,
- collection: 'comments'
- }
- ]
- })
-
- builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, {
- objectClass: tracker.class.Milestone,
- skip: [
- {
- _class: core.class.TxCollectionCUD,
- collection: 'comments'
- }
- ]
- })
-
- builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, {
- objectClass: tracker.class.Component,
- skip: [
- {
- _class: core.class.TxCollectionCUD,
- collection: 'comments'
- }
- ]
- })
-
- builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, {
- objectClass: tracker.class.IssueTemplate,
- skip: [
- {
- _class: core.class.TxCollectionCUD,
- collection: 'comments'
- }
- ]
- })
-
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: tracker.class.Issue,
components: { input: chunter.component.ChatMessageInput }
diff --git a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageAttributes.svelte b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageAttributes.svelte
index 9e2cd56531..a25f0cbbe8 100644
--- a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageAttributes.svelte
+++ b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageAttributes.svelte
@@ -16,20 +16,22 @@
import { Component } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { getClient } from '@hcengineering/presentation'
- import { DocAttributeUpdates, DocUpdateMessageViewlet } from '@hcengineering/activity'
+ import { DocAttributeUpdates, DocUpdateMessage, DocUpdateMessageViewlet } from '@hcengineering/activity'
+ import { Doc, Ref, Space } from '@hcengineering/core'
import activity from '../../plugin'
import AddedAttributesPresenter from './attributes/AddedAttributesPresenter.svelte'
import RemovedAttributesPresenter from './attributes/RemovedAttributesPresenter.svelte'
import SetAttributesPresenter from './attributes/SetAttributesPresenter.svelte'
- import { Ref, Space } from '@hcengineering/core'
export let viewlet: DocUpdateMessageViewlet | undefined
export let attributeUpdates: DocAttributeUpdates
export let attributeModel: AttributeModel
export let preview = false
export let space: Ref | undefined = undefined
+ export let object: Doc | undefined
+ export let message: DocUpdateMessage
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -41,7 +43,7 @@
{#if presenter}
-
+
{:else}
{#if attributeUpdates.added.length}
diff --git a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte
index 2c7e5b007c..4571dcaa6b 100644
--- a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte
+++ b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte
@@ -229,7 +229,14 @@
/>
{:else if value.attributeUpdates && attributeModel}
-
+
{/if}
diff --git a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePreview.svelte b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePreview.svelte
index 140ba05c86..1e3764af17 100644
--- a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePreview.svelte
+++ b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePreview.svelte
@@ -130,6 +130,8 @@
{attributeModel}
{space}
{viewlet}
+ {object}
+ message={value}
preview
/>
{/if}
diff --git a/plugins/activity/src/index.ts b/plugins/activity/src/index.ts
index 741311721c..256356380f 100644
--- a/plugins/activity/src/index.ts
+++ b/plugins/activity/src/index.ts
@@ -67,6 +67,7 @@ export interface ActivityMessageControl extends Doc {
// Skip field activity operations.
skipFields?: (keyof T)[]
+ allowedFields?: (keyof T)[]
}
/**
diff --git a/plugins/chunter-assets/lang/en.json b/plugins/chunter-assets/lang/en.json
index 47da2905af..72121e5360 100644
--- a/plugins/chunter-assets/lang/en.json
+++ b/plugins/chunter-assets/lang/en.json
@@ -111,6 +111,14 @@
"JoinChannelText": "Once you've joined, you'll be able to read all messages and contribute to the discussion.",
"NoMessagesInChannel": "Currently there are no messages",
"SendMessagesInChannel": "Send the first message to start the conversation",
- "LatestMessages": "↓ Latest messages"
+ "LatestMessages": "↓ Latest messages",
+ "Joined": "Joined",
+ "Left": "Left",
+ "Added": "Added",
+ "Removed": "Removed",
+ "CreatedChannelOn": "Created this channel on {date}",
+ "ChannelMessages": "Channel messages",
+ "JoinChannel": "Join channel",
+ "YouJoinedChannel": "You have been joined to channel"
}
}
\ No newline at end of file
diff --git a/plugins/chunter-assets/lang/es.json b/plugins/chunter-assets/lang/es.json
index aeb970861d..9cbd0877e7 100644
--- a/plugins/chunter-assets/lang/es.json
+++ b/plugins/chunter-assets/lang/es.json
@@ -111,6 +111,14 @@
"JoinChannelText": "Una vez que se haya unido, podrá leer todos los mensajes y contribuir a la discusión.",
"NoMessagesInChannel": "No hay mensajes en este canal todavía.",
"SendMessagesInChannel": "Envíe mensajes en este canal para comenzar la conversación.",
- "LatestMessages": "↓ Últimos mensajes"
+ "LatestMessages": "↓ Últimos mensajes",
+ "Joined": "Unido",
+ "Left": "Abandonado",
+ "Added": "Añadido",
+ "Removed": "Eliminado",
+ "CreatedChannelOn": "Creó este canal el {date}",
+ "ChannelMessages": "Mensajes del canal",
+ "JoinChannel": "Unirse",
+ "YouJoinedChannel": "Te has unido al canal"
}
}
\ No newline at end of file
diff --git a/plugins/chunter-assets/lang/fr.json b/plugins/chunter-assets/lang/fr.json
index 9a784ce568..fb42dd0389 100644
--- a/plugins/chunter-assets/lang/fr.json
+++ b/plugins/chunter-assets/lang/fr.json
@@ -111,6 +111,14 @@
"JoinChannelText": "Une fois que vous avez rejoint, vous pourrez lire tous les messages et participer à la discussion.",
"NoMessagesInChannel": "Il n'y a pas encore de messages dans ce canal.",
"SendMessagesInChannel": "Envoyez des messages pour commencer la conversation.",
- "LatestMessages": "↓ Derniers messages"
+ "LatestMessages": "↓ Derniers messages",
+ "Joined": "Rejoint",
+ "Left": "Quitté",
+ "Added": "Ajouté",
+ "Removed": "Supprimé",
+ "CreatedChannelOn": "A créé ce canal le {date}",
+ "ChannelMessages": "Messages du canal",
+ "JoinChannel": "Rejoindre",
+ "YouJoinedChannel": "Vous avez rejoint le canal"
}
}
\ No newline at end of file
diff --git a/plugins/chunter-assets/lang/pt.json b/plugins/chunter-assets/lang/pt.json
index 74633c8111..3758587fdf 100644
--- a/plugins/chunter-assets/lang/pt.json
+++ b/plugins/chunter-assets/lang/pt.json
@@ -111,6 +111,14 @@
"JoinChannelText": "Depois de entrar, você poderá ler todas as mensagens e contribuir na discussão.",
"NoMessagesInChannel": "Ainda não existem mensagens neste canal.",
"SendMessagesInChannel": "Envie a sua primeira mensagem!",
- "LatestMessages": "↓ Últimas mensagens"
+ "LatestMessages": "↓ Últimas mensagens",
+ "Joined": "Entrou",
+ "Left": "Saiu",
+ "Added": "Adicionado(a)",
+ "Removed": "Removido(a)",
+ "CreatedChannelOn": "Criou este canal em {date}",
+ "ChannelMessages": "Mensagens do canal",
+ "JoinChannel": "Participar no canal",
+ "YouJoinedChannel": "Entrou no canal"
}
}
\ No newline at end of file
diff --git a/plugins/chunter-assets/lang/ru.json b/plugins/chunter-assets/lang/ru.json
index e545f5bca9..f18301ff30 100644
--- a/plugins/chunter-assets/lang/ru.json
+++ b/plugins/chunter-assets/lang/ru.json
@@ -111,6 +111,14 @@
"JoinChannelText": "Присоединившись, вы сможете читать все сообщения и участвовать в обсуждении.",
"NoMessagesInChannel": "В этом канале пока нет сообщений",
"SendMessagesInChannel": "Отправьте первое сообщение, чтобы начать общение",
- "LatestMessages": "↓ Последние сообщения"
+ "LatestMessages": "↓ Последние сообщения",
+ "Joined": "Присоединился",
+ "Left": "Покинул",
+ "Added": "Добавил(а)",
+ "Removed": "Исключил(а)",
+ "CreatedChannelOn": "Создал этот канал {date}",
+ "ChannelMessages": "Сообщения каналов",
+ "JoinChannel": "Приссоединение к каналу",
+ "YouJoinedChannel": "Вы присоединились к каналу"
}
}
\ No newline at end of file
diff --git a/plugins/chunter-assets/lang/zh.json b/plugins/chunter-assets/lang/zh.json
index e182b46825..0da4296619 100644
--- a/plugins/chunter-assets/lang/zh.json
+++ b/plugins/chunter-assets/lang/zh.json
@@ -111,6 +111,14 @@
"JoinChannelText": "加入后,你将能够阅读所有消息并参与讨论。",
"NoMessagesInChannel": "此频道中没有消息。",
"SendMessagesInChannel": "在此频道中发送消息。",
- "LatestMessages": "↓ 最新消息"
+ "LatestMessages": "↓ 最新消息",
+ "Joined": "已加入",
+ "Left": "已离开",
+ "Added": "已添加",
+ "Removed": "已移除",
+ "CreatedChannelOn": "于 {date} 创建此频道",
+ "ChannelMessages": "频道消息",
+ "JoinChannel": "加入频道",
+ "YouJoinedChannel": "你已加入频道"
}
}
diff --git a/plugins/chunter-resources/src/channelDataProvider.ts b/plugins/chunter-resources/src/channelDataProvider.ts
index 7334a5ad11..db3126ba62 100644
--- a/plugins/chunter-resources/src/channelDataProvider.ts
+++ b/plugins/chunter-resources/src/channelDataProvider.ts
@@ -29,11 +29,8 @@ import { derived, get, type Readable, writable } from 'svelte/store'
import activity, { type ActivityMessage, type ActivityReference } from '@hcengineering/activity'
import attachment from '@hcengineering/attachment'
import { combineActivityMessages, sortActivityMessages } from '@hcengineering/activity-resources'
-import { type ChatMessage } from '@hcengineering/chunter'
import notification, { type DocNotifyContext } from '@hcengineering/notification'
-import chunter from './plugin'
-
export type LoadMode = 'forward' | 'backward'
export interface MessageMetadata {
@@ -241,7 +238,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
if (loadAll) {
this.isTailLoading.set(true)
- this.loadTail(undefined, combineActivityMessages)
+ this.loadTail()
} else if (isLoadingLatest) {
const startIndex = Math.max(0, count - this.limit)
this.isTailLoading.set(true)
@@ -260,11 +257,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
this.isInitialLoadedStore.set(true)
}
- private loadTail (
- start?: Timestamp,
- afterLoad?: (msgs: ActivityMessage[]) => Promise,
- query?: DocumentQuery
- ): void {
+ private loadTail (start?: Timestamp, query?: DocumentQuery): void {
if (this.chatId === undefined) {
this.isTailLoading.set(false)
return
@@ -283,12 +276,8 @@ export class ChannelDataProvider implements IChannelDataProvider {
...(this.tailStart !== undefined ? { createdOn: { $gte: this.tailStart } } : {})
},
async (res) => {
- if (afterLoad !== undefined) {
- const result = await afterLoad(res.reverse())
- this.tailStore.set(result)
- } else {
- this.tailStore.set(res.reverse())
- }
+ const result = await combineActivityMessages(res.reverse())
+ this.tailStore.set(result)
this.isTailLoaded.set(true)
this.isTailLoading.set(false)
@@ -325,7 +314,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
const skipIds = this.getChunkSkipIds(loadAfter)
const messages = await client.findAll(
- chunter.class.ChatMessage,
+ this.msgClass,
{
attachedTo: this.chatId,
space: this.space,
@@ -351,11 +340,11 @@ export class ChannelDataProvider implements IChannelDataProvider {
return {
from: from.createdOn ?? from.modifiedOn,
to: to.createdOn ?? to.modifiedOn,
- data: isBackward ? messages.reverse() : messages
+ data: isBackward ? await combineActivityMessages(messages.reverse()) : await combineActivityMessages(messages)
}
}
- getChunkSkipIds (after: Timestamp, loadTail = false): Array[> {
+ getChunkSkipIds (after: Timestamp, loadTail = false): Array][> {
const chunks = get(this.chunksStore)
const metadata = get(this.metadataStore)
const tail = get(this.tailStore)
@@ -367,7 +356,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
.flat()
.concat(loadTail ? [] : tailData)
.filter(({ createdOn }) => createdOn === after)
- .map(({ _id }) => _id) as Array][>
+ .map(({ _id }) => _id)
}
async loadNext (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise {
@@ -467,7 +456,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
if (tailAfter !== undefined) {
const skipIds = chunks[chunks.length - 1]?.data.map(({ _id }) => _id) ?? []
- this.loadTail(tailAfter, undefined, { _id: { $nin: skipIds } })
+ this.loadTail(tailAfter, { _id: { $nin: skipIds } })
this.isLoadingMoreStore.set(false)
return
}
diff --git a/plugins/chunter-resources/src/components/Channel.svelte b/plugins/chunter-resources/src/components/Channel.svelte
index 656489358c..966c148705 100644
--- a/plugins/chunter-resources/src/components/Channel.svelte
+++ b/plugins/chunter-resources/src/components/Channel.svelte
@@ -13,7 +13,7 @@
// limitations under the License.
-->
+
+{#if value}
+
+
+
+
+{/if}
+
+
diff --git a/plugins/chunter-resources/src/components/activity/MembersChangedMessage.svelte b/plugins/chunter-resources/src/components/activity/MembersChangedMessage.svelte
new file mode 100644
index 0000000000..650b271481
--- /dev/null
+++ b/plugins/chunter-resources/src/components/activity/MembersChangedMessage.svelte
@@ -0,0 +1,128 @@
+
+
+
+
+
+ {#if addedPersons.length > 0}
+
+ {#if isJoined}
+
+
+
+ {object?.name}
+ {:else}
+
+ {#each addedPersons as person, index}
+
+ {#if index < addedPersons.length - 1}
+ ,
+ {/if}
+ {/each}
+ {/if}
+
+ {/if}
+ {#if differentActions}
+
+ {/if}
+
+ {#if removedPersons.length > 0}
+
+ {#if isLeave}
+
+
+
+ {object?.name}
+ {:else}
+
+
+
+ {#each removedPersons as person, index}
+
+ {#if index < removedPersons.length - 1}
+ ,
+ {/if}
+ {/each}
+ {/if}
+
+ {/if}
+
+
+
diff --git a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte
index a524e646bc..83a6fc5255 100644
--- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte
+++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte
@@ -14,7 +14,7 @@
-->
+
+
diff --git a/plugins/chunter-resources/src/index.ts b/plugins/chunter-resources/src/index.ts
index 9e7bb6719e..c053acb199 100644
--- a/plugins/chunter-resources/src/index.ts
+++ b/plugins/chunter-resources/src/index.ts
@@ -48,6 +48,9 @@ import ChatMessageNotificationLabel from './components/notification/ChatMessageN
import ChatAside from './components/chat/ChatAside.svelte'
import ThreadMessagePreview from './components/threads/ThreadMessagePreview.svelte'
import ChatMessagePreview from './components/chat-message/ChatMessagePreview.svelte'
+import ChannelCreatedMessage from './components/activity/ChannelCreatedMessage.svelte'
+import MembersChangedMessage from './components/activity/MembersChangedMessage.svelte'
+import JoinChannelNotificationPresenter from './components/notification/JoinChannelNotificationPresenter.svelte'
import {
ChannelTitleProvider,
@@ -177,7 +180,12 @@ export default async (): Promise => ({
ThreadNotificationPresenter,
ChatAside,
ThreadMessagePreview,
- ChatMessagePreview
+ ChatMessagePreview,
+ JoinChannelNotificationPresenter
+ },
+ activity: {
+ ChannelCreatedMessage,
+ MembersChangedMessage
},
function: {
GetDmName: getDmName,
diff --git a/plugins/chunter-resources/src/navigation.ts b/plugins/chunter-resources/src/navigation.ts
index 06c02167aa..5dd14280c7 100644
--- a/plugins/chunter-resources/src/navigation.ts
+++ b/plugins/chunter-resources/src/navigation.ts
@@ -1,4 +1,11 @@
-import { getCurrentLocation, getCurrentResolvedLocation, getLocation, type Location, navigate } from '@hcengineering/ui'
+import {
+ closePanel,
+ getCurrentLocation,
+ getCurrentResolvedLocation,
+ getLocation,
+ type Location,
+ navigate
+} from '@hcengineering/ui'
import { type Ref, type Doc, type Class } from '@hcengineering/core'
import type { ActivityMessage } from '@hcengineering/activity'
import { chunterId, type ChunterSpace, type ThreadMessage } from '@hcengineering/chunter'
@@ -184,5 +191,6 @@ export async function resetChunterLocIfEqual (_id: Ref, _class: Ref,
ThreadNotification: '' as Ref,
ChannelNotification: '' as Ref,
+ JoinChannelNotification: '' as Ref,
ThreadMessageViewlet: '' as Ref
},
app: {
diff --git a/plugins/contact-resources/src/components/PersonContent.svelte b/plugins/contact-resources/src/components/PersonContent.svelte
index 55e4fed58a..2964d891b1 100644
--- a/plugins/contact-resources/src/components/PersonContent.svelte
+++ b/plugins/contact-resources/src/components/PersonContent.svelte
@@ -50,6 +50,7 @@
export let compact: boolean = false
export let showStatus: boolean = false
export let type: ObjectPresenterType = 'link'
+ export let overflowLabel = true
const client = getClient()
@@ -93,6 +94,7 @@
{type}
{maxWidth}
{showStatus}
+ {overflowLabel}
/>
@@ -116,6 +118,7 @@
{type}
{maxWidth}
{showStatus}
+ {overflowLabel}
/>
{/if}
{:else if shouldShowPlaceholder}
diff --git a/plugins/contact-resources/src/components/PersonElement.svelte b/plugins/contact-resources/src/components/PersonElement.svelte
index 4331c65672..8fb3eb2f8a 100644
--- a/plugins/contact-resources/src/components/PersonElement.svelte
+++ b/plugins/contact-resources/src/components/PersonElement.svelte
@@ -38,6 +38,7 @@
export let maxWidth: string = ''
export let type: ObjectPresenterType = 'link'
export let showStatus = true
+ export let overflowLabel = true
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -67,14 +68,14 @@
{/if}
{#if shouldShowName}
-
+
{name}
{/if}
{:else if type === 'text'}
-
+
{name}
{/if}
diff --git a/plugins/contact-resources/src/components/PersonPresenter.svelte b/plugins/contact-resources/src/components/PersonPresenter.svelte
index 8e8b438150..3674461c1d 100644
--- a/plugins/contact-resources/src/components/PersonPresenter.svelte
+++ b/plugins/contact-resources/src/components/PersonPresenter.svelte
@@ -43,6 +43,7 @@
export let compact = false
export let type: ObjectPresenterType = 'link'
export let showStatus: boolean = false
+ export let overflowLabel = true
const client = getClient()
$: personValue = typeof value === 'string' ? $personByIdStore.get(value) : value
@@ -99,6 +100,7 @@
{compact}
{type}
{showStatus}
+ {overflowLabel}
on:accent-color
/>
{/if}
diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts
index 4fd9a7ea6a..2d51edccfd 100644
--- a/plugins/notification-resources/src/utils.ts
+++ b/plugins/notification-resources/src/utils.ts
@@ -83,7 +83,8 @@ export function loadNotificationSettings (): void {
providersSettings.set(res)
}
)
- typeSettingsQuery.query(notification.class.NotificationTypeSetting, {}, (res) => {
+
+ typeSettingsQuery.query(notification.class.NotificationTypeSetting, { space: core.space.Workspace }, (res) => {
typesSettings.set(res)
})
}
diff --git a/plugins/view-resources/src/plugin.ts b/plugins/view-resources/src/plugin.ts
index ed8e8cdc50..292131de8d 100644
--- a/plugins/view-resources/src/plugin.ts
+++ b/plugins/view-resources/src/plugin.ts
@@ -88,7 +88,6 @@ export default mergeIds(viewId, view, {
AfterDate: '' as IntlString,
BetweenDates: '' as IntlString,
SaveAs: '' as IntlString,
- And: '' as IntlString,
Between: '' as IntlString,
ShowColors: '' as IntlString,
Show: '' as IntlString,
diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts
index fc8463d468..df90297256 100644
--- a/plugins/view/src/index.ts
+++ b/plugins/view/src/index.ts
@@ -209,7 +209,8 @@ const view = plugin(viewId, {
Unpin: '' as IntlString,
Join: '' as IntlString,
Leave: '' as IntlString,
- Copied: '' as IntlString
+ Copied: '' as IntlString,
+ And: '' as IntlString
},
icon: {
Table: '' as Asset,
diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts
index b4c5992a7f..e8f2e096a9 100644
--- a/server-plugins/activity-resources/src/index.ts
+++ b/server-plugins/activity-resources/src/index.ts
@@ -150,8 +150,7 @@ export async function createReactionNotifications (
tx,
parentMessage,
[docUpdateMessage],
- { isOwn: true, isSpace: false, shouldUpdateTimestamp: false },
- new Map()
+ { isOwn: true, isSpace: false, shouldUpdateTimestamp: false }
)
)
@@ -264,12 +263,11 @@ export async function generateDocUpdateMessages (
originTx?: TxCUD,
objectCache?: DocObjectCache
): Promise[]> {
- const { hierarchy } = control
-
if (tx.space === core.space.DerivedTx) {
return res
}
+ const { hierarchy } = control
const etx = TxProcessor.extractTx(tx) as TxCUD
if (
@@ -321,6 +319,7 @@ export async function generateDocUpdateMessages (
let doc = objectCache?.docs?.get(tx.objectId)
if (doc === undefined) {
doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
+ objectCache?.docs?.set(tx.objectId, doc)
}
return await ctx.with(
'pushDocUpdateMessages',
@@ -349,6 +348,7 @@ export async function generateDocUpdateMessages (
let doc = objectCache?.docs?.get(tx.objectId)
if (doc === undefined) {
doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
+ objectCache?.docs?.set(tx.objectId, doc)
}
if (doc !== undefined) {
return await ctx.with(
@@ -376,33 +376,35 @@ export async function generateDocUpdateMessages (
}
async function ActivityMessagesHandler (tx: TxCUD, control: TriggerControl): Promise {
- if (tx.space === core.space.DerivedTx) {
- return []
- }
-
if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) {
return []
}
+ const cache: DocObjectCache = {
+ docs: new Map(),
+ transactions: new Map()
+ }
const txes = await control.ctx.with(
'generateDocUpdateMessages',
{},
- async (ctx) => await generateDocUpdateMessages(ctx, tx, control)
+ async (ctx) => await generateDocUpdateMessages(ctx, tx, control, [], undefined, cache)
)
- if (txes.length === 0) {
- return []
- }
-
const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc))
const notificationTxes = await control.ctx.with(
'createCollaboratorNotifications',
{},
- async (ctx) => await createCollaboratorNotifications(ctx, tx, control, messages)
+ async (ctx) =>
+ await createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map][, Doc>)
)
- return [...txes, ...notificationTxes]
+ const result = [...txes, ...notificationTxes]
+
+ if (result.length > 0) {
+ await control.apply(result)
+ }
+ return []
}
async function OnDocRemoved (originTx: TxCUD, control: TriggerControl): Promise {
diff --git a/server-plugins/activity-resources/src/utils.ts b/server-plugins/activity-resources/src/utils.ts
index 23070abd10..1617adcd1c 100644
--- a/server-plugins/activity-resources/src/utils.ts
+++ b/server-plugins/activity-resources/src/utils.ts
@@ -250,14 +250,12 @@ export async function getTxAttributesUpdates (
const hierarchy = control.hierarchy
- const filterSet = new Set()
- for (const c of controlRules ?? []) {
- for (const f of c.skipFields ?? []) {
- filterSet.add(f)
- }
- }
+ const allowedFields = new Set(controlRules?.flatMap((it) => it.allowedFields ?? []) ?? [])
+ const skipFields = new Set(controlRules?.flatMap((it) => it.skipFields ?? []) ?? [])
- const keys = getAvailableAttributesKeys(tx, hierarchy).filter((it) => !filterSet.has(it))
+ const keys = getAvailableAttributesKeys(tx, hierarchy).filter(
+ (it) => !skipFields.has(it) && (allowedFields.size === 0 || allowedFields.has(it))
+ )
if (keys.length === 0) {
return []
@@ -268,14 +266,7 @@ export async function getTxAttributesUpdates (
const isMixin = hierarchy.isDerived(tx._class, core.class.TxMixin)
const mixin = isMixin ? (tx as TxMixin).mixin : undefined
- const { doc, prevDoc } = await getDocDiff(
- control,
- updateObject._class,
- updateObject._id,
- originTx._id,
- mixin,
- objectCache
- )
+ let docDiff: { doc?: Doc, prevDoc?: Doc } | undefined
for (const key of keys) {
let attrValue = modifiedAttributes[key]
@@ -302,14 +293,25 @@ export async function getTxAttributesUpdates (
continue
}
- if (Array.isArray(attrValue) && doc != null) {
- const diff = await getAttributeDiff(control, doc, prevDoc, key, attrClass, isMixin)
+ if (
+ hierarchy.isDerived(attrClass, core.class.TypeMarkup) ||
+ hierarchy.isDerived(attrClass, core.class.TypeCollaborativeMarkup) ||
+ mixin === notification.mixin.Collaborators
+ ) {
+ if (docDiff === undefined) {
+ docDiff = await getDocDiff(control, updateObject._class, updateObject._id, originTx._id, mixin, objectCache)
+ }
+ }
+
+ if (Array.isArray(attrValue) && docDiff?.doc !== undefined) {
+ const diff = await getAttributeDiff(control, docDiff.doc, docDiff.prevDoc, key, attrClass, isMixin)
added.push(...diff.added)
removed.push(...diff.removed)
attrValue = []
}
- if (prevDoc !== undefined) {
+ if (docDiff?.prevDoc !== undefined) {
+ const { prevDoc } = docDiff
const rawPrevValue = isMixin ? (hierarchy.as(prevDoc, attrClass) as any)[key] : (prevDoc as any)[key]
if (Array.isArray(rawPrevValue)) {
diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts
index 5ef6cfbbd0..3d5434a826 100644
--- a/server-plugins/chunter-resources/src/index.ts
+++ b/server-plugins/chunter-resources/src/index.ts
@@ -45,7 +45,7 @@ import core, {
TxUpdateDoc,
UserStatus
} from '@hcengineering/core'
-import notification, { Collaborators, DocNotifyContext, NotificationContent } from '@hcengineering/notification'
+import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification'
import { getMetadata, IntlString, translate } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import {
@@ -288,13 +288,6 @@ export async function ChunterTrigger (tx: TxCUD, control: TriggerControl):
res.push(
...(await control.ctx.with('OnThreadMessageDeleted', {}, async (ctx) => await OnThreadMessageDeleted(tx, control)))
)
- res.push(
- ...(await control.ctx.with(
- 'OnCollaboratorsChanged',
- {},
- async (ctx) => await OnCollaboratorsChanged(tx as TxMixin, control)
- ))
- )
res.push(
...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control)))
)
@@ -381,104 +374,6 @@ function combineAttributes (attributes: any[], key: string, operator: string, ar
).filter((v) => v != null)
}
-async function OnChannelMembersChanged (tx: TxUpdateDoc, control: TriggerControl): Promise {
- const changedAttributes = Object.entries(tx.operations)
- .flatMap(([id, val]) => (['$push', '$pull'].includes(id) ? Object.keys(val) : id))
- .filter((id) => !id.startsWith('$'))
-
- if (!changedAttributes.includes('members')) {
- return []
- }
-
- const added = combineAttributes([tx.operations], 'members', '$push', '$each')
- const removed = combineAttributes([tx.operations], 'members', '$pull', '$in')
-
- const res: Tx[] = []
- const allContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: tx.objectId })
-
- if (removed.length > 0) {
- res.push(
- control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, notification.mixin.Collaborators, {
- $pull: {
- collaborators: { $in: removed }
- }
- })
- )
- }
-
- if (added.length > 0) {
- res.push(
- control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, notification.mixin.Collaborators, {
- $push: {
- collaborators: { $each: added, $position: 0 }
- }
- })
- )
- }
-
- const accounts =
- added.length > 0 ? await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: added } }) : []
- const spaces =
- accounts.length > 0
- ? await control.findAll(contact.class.PersonSpace, { person: { $in: accounts.map((x) => x.person) } })
- : []
-
- for (const addedMember of added) {
- const context = allContexts.find(({ user }) => user === addedMember)
-
- if (context === undefined) {
- const account = accounts.find(({ _id }) => _id === addedMember)
- if (account === undefined) continue
- const space = spaces.find(({ person }) => person === account.person)
- if (space === undefined) continue
- const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, space._id, {
- objectId: tx.objectId,
- objectClass: tx.objectClass,
- objectSpace: tx.objectSpace,
- user: addedMember,
- lastViewedTimestamp: tx.modifiedOn,
- isPinned: false
- })
-
- await control.apply([createTx])
- } else {
- const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
- lastViewedTimestamp: tx.modifiedOn
- })
-
- res.push(updateTx)
- }
- }
-
- const contextsToRemove = allContexts.filter(({ user }) => removed.includes(user))
-
- for (const context of contextsToRemove) {
- res.push(control.txFactory.createTxRemoveDoc(context._class, context.space, context._id))
- }
-
- return res
-}
-
-async function OnCollaboratorsChanged (tx: TxMixin, control: TriggerControl): Promise {
- if (tx._class !== core.class.TxMixin || tx.mixin !== notification.mixin.Collaborators) return []
-
- if (!control.hierarchy.isDerived(tx.objectClass, chunter.class.Channel)) return []
-
- const doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }))[0] as Channel | undefined
-
- if (doc === undefined) return []
- if (doc.private) return []
-
- const added = combineAttributes([tx.attributes], 'collaborators', '$push', '$each')
- const res: Tx[] = []
-
- for (const addedMember of added) {
- res.push(...joinChannel(control, doc, addedMember))
- }
-
- return res
-}
-
async function hideOldDirects (
directs: DocNotifyContext[],
control: TriggerControl,
@@ -582,7 +477,7 @@ export async function updateChatInfo (control: TriggerControl, status: UserStatu
({ objectClass }) =>
!hierarchy.isDerived(objectClass, chunter.class.DirectMessage) &&
!hierarchy.isDerived(objectClass, chunter.class.Channel) &&
- !hierarchy.isDerived(objectClass, chunter.class.Channel)
+ !hierarchy.isDerived(objectClass, activity.class.ActivityMessage)
)
const directTxes = await hideOldDirects(directContexts, control, date)
@@ -661,12 +556,21 @@ async function OnContextUpdate (tx: TxUpdateDoc, control: Trig
return []
}
+async function JoinChannelTypeMatch (originTx: Tx, _: Doc, user: Ref): Promise {
+ if (originTx.modifiedBy === user) return false
+ if (originTx._class !== core.class.TxUpdateDoc) return false
+
+ const tx = originTx as TxUpdateDoc
+ const added = combineAttributes([tx.operations], 'members', '$push', '$each')
+
+ return added.includes(user)
+}
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
ChunterTrigger,
OnChatMessageRemoved,
- OnChannelMembersChanged,
ChatNotificationsHandler,
OnUserStatus,
OnContextUpdate
@@ -676,6 +580,7 @@ export default async () => ({
ChannelHTMLPresenter: channelHTMLPresenter,
ChannelTextPresenter: channelTextPresenter,
ChunterNotificationContentProvider: getChunterNotificationContent,
- ChatMessageTextPresenter
+ ChatMessageTextPresenter,
+ JoinChannelTypeMatch
}
})
diff --git a/server-plugins/chunter/src/index.ts b/server-plugins/chunter/src/index.ts
index 4c800ff204..d32a731e4f 100644
--- a/server-plugins/chunter/src/index.ts
+++ b/server-plugins/chunter/src/index.ts
@@ -16,7 +16,7 @@
import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { ObjectDDParticipantFunc, TriggerFunc } from '@hcengineering/server-core'
-import { NotificationContentProvider, Presenter } from '@hcengineering/server-notification'
+import { NotificationContentProvider, Presenter, TypeMatchFunc } from '@hcengineering/server-notification'
/**
* @public
@@ -30,7 +30,6 @@ export default plugin(serverChunterId, {
trigger: {
ChunterTrigger: '' as Resource,
OnChatMessageRemoved: '' as Resource,
- OnChannelMembersChanged: '' as Resource,
ChatNotificationsHandler: '' as Resource,
OnUserStatus: '' as Resource,
OnContextUpdate: '' as Resource
@@ -40,6 +39,7 @@ export default plugin(serverChunterId, {
ChannelHTMLPresenter: '' as Resource,
ChannelTextPresenter: '' as Resource,
ChunterNotificationContentProvider: '' as Resource,
- ChatMessageTextPresenter: '' as Resource
+ ChatMessageTextPresenter: '' as Resource,
+ JoinChannelTypeMatch: '' as TypeMatchFunc
}
})
diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts
index e6a2b77002..eb87ef3110 100644
--- a/server-plugins/notification-resources/src/index.ts
+++ b/server-plugins/notification-resources/src/index.ts
@@ -87,6 +87,8 @@ import webpush, { WebPushError } from 'web-push'
import { Content, NotifyParams, NotifyResult } from './types'
import {
+ createPullCollaboratorsTx,
+ createPushCollaboratorsTx,
getHTMLPresenter,
getNotificationContent,
getTextPresenter,
@@ -269,13 +271,13 @@ async function getValueCollaborators (value: any, attr: AnyAttribute, control: T
}
async function getKeyCollaborators (
- doc: Doc,
+ docClass: Ref>,
value: any,
field: string,
control: TriggerControl
): Promise][[] | undefined> {
if (value !== undefined && value !== null) {
- const attr = control.hierarchy.findAttribute(doc._class, field)
+ const attr = control.hierarchy.findAttribute(docClass, field)
if (attr !== undefined) {
return await getValueCollaborators(value, attr, control)
}
@@ -297,7 +299,7 @@ export async function getDocCollaborators (
const newCollaborators = await ctx.with(
'getKeyCollaborators',
{},
- async () => await getKeyCollaborators(doc, value, field, control)
+ async () => await getKeyCollaborators(doc._class, value, field, control)
)
if (newCollaborators !== undefined) {
for (const newCollaborator of newCollaborators) {
@@ -321,35 +323,25 @@ export async function pushInboxNotifications (
modifiedOn: Timestamp,
shouldUpdateTimestamp = true
): Promise | undefined> {
- const account = receiver.account
const context = contexts.find((context) => context.user === receiver._id && context.objectId === objectId)
let docNotifyContextId: Ref
if (context === undefined) {
- const createContextTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, receiver.space, {
- user: receiver._id,
+ docNotifyContextId = await createNotifyContext(
+ control,
objectId,
objectClass,
objectSpace,
- isPinned: false,
- lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined
- })
- await control.apply([createContextTx])
- if (receiver.account?.email !== undefined) {
- control.operationContext.derived.targets['docNotifyContext' + createContextTx._id] = (it) => {
- if (it._id === createContextTx._id) {
- return [receiver.account?.email]
- }
- }
- }
- docNotifyContextId = createContextTx.objectId
+ receiver,
+ shouldUpdateTimestamp ? modifiedOn : undefined
+ )
} else {
docNotifyContextId = context._id
}
const notificationData = {
- user: account._id,
+ user: receiver._id,
isViewed: false,
docNotifyContext: docNotifyContextId,
archived: false,
@@ -630,7 +622,6 @@ export async function applyNotificationProviders (
sender,
data._id
)
- // console.log('Push takes', Date.now() - now, 'ms')
if (pushTx !== undefined) {
res.push(pushTx)
}
@@ -651,6 +642,33 @@ export async function applyNotificationProviders (
}
}
+async function createNotifyContext (
+ control: TriggerControl,
+ objectId: Ref,
+ objectClass: Ref>,
+ objectSpace: Ref,
+ receiver: ReceiverInfo,
+ updateTimestamp?: Timestamp
+): Promise][> {
+ const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, receiver.space, {
+ user: receiver._id,
+ objectId,
+ objectClass,
+ objectSpace,
+ isPinned: false,
+ lastUpdateTimestamp: updateTimestamp
+ })
+ await control.apply([createTx])
+ if (receiver.account?.email !== undefined) {
+ control.operationContext.derived.targets['docNotifyContext' + createTx._id] = (it) => {
+ if (it._id === createTx._id) {
+ return [receiver.account?.email]
+ }
+ }
+ }
+ return createTx.objectId
+}
+
export async function getNotificationTxes (
control: TriggerControl,
object: Doc,
@@ -709,6 +727,21 @@ export async function getNotificationTxes (
sender
)
}
+ } else {
+ const context = docNotifyContexts.find(
+ (context) => context.objectId === message.attachedTo && context.user === receiver.account._id
+ )
+
+ if (context === undefined) {
+ await createNotifyContext(
+ control,
+ message.attachedTo,
+ message.attachedToClass,
+ message.space,
+ receiver,
+ params.shouldUpdateTimestamp ? originTx.modifiedOn : undefined
+ )
+ }
}
}
return res
@@ -754,6 +787,39 @@ async function updateContextsTimestamp (
await control.apply(res)
}
+async function removeContexts (
+ contexts: DocNotifyContext[],
+ unsubscribe: Ref[],
+ control: TriggerControl
+): Promise {
+ if (contexts.length === 0) return
+ if (unsubscribe.length === 0) return
+
+ const unsubscribeAccounts = await control.modelDb.findAll(contact.class.PersonAccount, {
+ _id: { $in: unsubscribe }
+ })
+ const res: Tx[] = []
+
+ for (const context of contexts) {
+ const account = unsubscribeAccounts.find(({ _id }) => _id === context.user)
+ if (account === undefined) continue
+
+ const removeTx = control.txFactory.createTxRemoveDoc(context._class, context.space, context._id)
+
+ res.push(removeTx)
+
+ if (account.email !== undefined) {
+ control.operationContext.derived.targets['docNotifyContext' + removeTx._id] = (it) => {
+ if (it._id === removeTx._id) {
+ return [account.email]
+ }
+ }
+ }
+ }
+
+ await control.apply(res)
+}
+
export async function createCollabDocInfo (
ctx: MeasureContext,
collaborators: Ref[],
@@ -763,7 +829,7 @@ export async function createCollabDocInfo (
object: Doc,
activityMessages: ActivityMessage[],
params: NotifyParams,
- cache: Map][, Doc>
+ unsubscribe: Ref[] = []
): Promise {
let res: Tx[] = []
@@ -774,6 +840,7 @@ export async function createCollabDocInfo (
const notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { objectId: object._id })
await updateContextsTimestamp(notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy)
+ await removeContexts(notifyContexts, unsubscribe, control)
const docMessages = activityMessages.filter((message) => message.attachedTo === object._id)
if (docMessages.length === 0) {
@@ -855,6 +922,50 @@ export function getMixinTx (
)
}
+async function getTxCollabs (
+ ctx: MeasureContext,
+ tx: TxCUD,
+ control: TriggerControl,
+ doc: Doc
+): Promise<{
+ added: Ref[]
+ removed: Ref[]
+ result: Ref[]
+ }> {
+ const { hierarchy } = control
+ const mixin = hierarchy.classHierarchyMixin(
+ doc._class,
+ notification.mixin.ClassCollaborators
+ )
+ if (mixin === undefined) return { added: [], removed: [], result: [] }
+
+ if (tx._class === core.class.TxCreateDoc) {
+ const collabs = await getDocCollaborators(ctx, doc, mixin, control)
+ return { added: collabs, removed: [], result: collabs }
+ }
+
+ if (tx._class === core.class.TxRemoveDoc) {
+ if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) {
+ return { added: [], removed: [], result: hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [] }
+ }
+
+ return { added: [], removed: [], result: [] }
+ }
+
+ if ([core.class.TxUpdateDoc, core.class.TxMixin].includes(tx._class)) {
+ const collabs = new Set(hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [])
+ const ops = isMixinTx(tx) ? tx.attributes : (tx as TxUpdateDoc).operations
+ const newCollaborators = (await getNewCollaborators(ops, mixin, doc._class, control)).filter((p) => !collabs.has(p))
+ const isSpace = control.hierarchy.isDerived(doc._class, core.class.Space)
+ const removedCollabs = isSpace ? await getRemovedMembers(ops, mixin, (doc as Space)._class, control) : []
+ const result = [...collabs, ...newCollaborators].filter((p) => !removedCollabs.includes(p))
+
+ return { added: newCollaborators, removed: removedCollabs, result }
+ }
+
+ return { added: [], removed: [], result: [] }
+}
+
async function getSpaceCollabTxes (
control: TriggerControl,
doc: Doc,
@@ -887,8 +998,7 @@ async function getSpaceCollabTxes (
originTx,
doc,
activityMessages,
- { isSpace: true, isOwn: false, shouldUpdateTimestamp: true },
- cache
+ { isSpace: true, isOwn: false, shouldUpdateTimestamp: true }
)
}
}
@@ -931,8 +1041,7 @@ async function createCollaboratorDoc (
originTx,
doc,
activityMessage,
- { isOwn: true, isSpace: false, shouldUpdateTimestamp: true },
- cache
+ { isOwn: true, isSpace: false, shouldUpdateTimestamp: true }
)
)
res.push(mixinTx)
@@ -1105,8 +1214,7 @@ async function collectionCollabDoc (
tx,
doc,
activityMessages,
- { isOwn: false, isSpace: false, shouldUpdateTimestamp: true },
- cache
+ { isOwn: false, isSpace: false, shouldUpdateTimestamp: true }
)
)
)
@@ -1168,7 +1276,7 @@ async function removeCollaboratorDoc (tx: TxRemoveDoc, control: TriggerCont
async function getNewCollaborators (
ops: DocumentUpdate | MixinUpdate,
mixin: ClassCollaborators,
- doc: Doc,
+ docClass: Ref>,
control: TriggerControl
): Promise][[]> {
const newCollaborators = new Set][>()
@@ -1179,7 +1287,7 @@ async function getNewCollaborators (
if (typeof value !== 'string') {
value = value.$each
}
- const newCollabs = await getKeyCollaborators(doc, value, key, control)
+ const newCollabs = await getKeyCollaborators(docClass, value, key, control)
if (newCollabs !== undefined) {
for (const newCollab of newCollabs) {
newCollaborators.add(newCollab)
@@ -1192,7 +1300,7 @@ async function getNewCollaborators (
if (key.startsWith('$')) continue
if (mixin.fields.includes(key)) {
const value = (ops as any)[key]
- const newCollabs = await getKeyCollaborators(doc, value, key, control)
+ const newCollabs = await getKeyCollaborators(docClass, value, key, control)
if (newCollabs !== undefined) {
for (const newCollab of newCollabs) {
newCollaborators.add(newCollab)
@@ -1203,6 +1311,30 @@ async function getNewCollaborators (
return Array.from(newCollaborators.values())
}
+async function getRemovedMembers (
+ ops: DocumentUpdate | MixinUpdate,
+ mixin: ClassCollaborators,
+ docClass: Ref>,
+ control: TriggerControl
+): Promise][[]> {
+ const removedCollaborators: Ref[] = []
+ if (ops.$pull !== undefined && 'members' in ops.$pull) {
+ const key = 'members'
+ if (mixin.fields.includes(key)) {
+ let value = (ops.$pull as any)[key]
+ if (typeof value !== 'string') {
+ value = value.$in
+ }
+ const collabs = await getKeyCollaborators(docClass, value, key, control)
+ if (collabs !== undefined) {
+ removedCollaborators.push(...collabs)
+ }
+ }
+ }
+
+ return Array.from(new Set(removedCollaborators).values())
+}
+
async function updateCollaboratorDoc (
ctx: MeasureContext,
tx: TxUpdateDoc | TxMixin,
@@ -1218,31 +1350,28 @@ async function updateCollaboratorDoc (
const doc = await ctx.with(
'find-doc',
{ _class: tx.objectClass },
- async () => (await control.findAllCtx(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
+ async () =>
+ cache.get(tx.objectId) ?? (await control.findAllCtx(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
)
if (doc === undefined) return []
+ cache.set(doc._id, doc)
const params: NotifyParams = { isOwn: true, isSpace: false, shouldUpdateTimestamp: true }
if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) {
// we should handle change field and subscribe new collaborators
- const collabMixin = hierarchy.as(doc, notification.mixin.Collaborators)
- const collabs = new Set(collabMixin.collaborators)
- const ops = isMixinTx(tx) ? tx.attributes : tx.operations
- const newCollaborators = await ctx.with('get-new-collaborators', {}, async () =>
- (await getNewCollaborators(ops, mixin, doc, control)).filter((p) => !collabs.has(p))
+ const collabsInfo = await ctx.with(
+ 'get-tx-collaborators',
+ {},
+ async () => await getTxCollabs(ctx, tx, control, doc)
)
- if (newCollaborators.length > 0) {
- res.push(
- control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, notification.mixin.Collaborators, {
- $push: {
- collaborators: {
- $each: newCollaborators,
- $position: 0
- }
- }
- })
- )
+ if (collabsInfo.added.length > 0) {
+ res.push(createPushCollaboratorsTx(control, tx.objectId, tx.objectClass, tx.objectSpace, collabsInfo.added))
}
+
+ if (collabsInfo.removed.length > 0) {
+ res.push(createPullCollaboratorsTx(control, tx.objectId, tx.objectClass, tx.objectSpace, collabsInfo.removed))
+ }
+
res = res.concat(
await ctx.with(
'create-collab-docinfo',
@@ -1250,14 +1379,14 @@ async function updateCollaboratorDoc (
async () =>
await createCollabDocInfo(
ctx,
- [...collabMixin.collaborators, ...newCollaborators] as Ref[],
+ collabsInfo.result as Ref[],
control,
tx,
originTx,
doc,
activityMessages,
params,
- cache
+ collabsInfo.removed as Ref[]
)
)
)
@@ -1277,8 +1406,7 @@ async function updateCollaboratorDoc (
originTx,
doc,
activityMessages,
- params,
- cache
+ params
)
)
}
@@ -1409,6 +1537,59 @@ async function applyUserTxes (
return res
}
+async function updateCollaborators (ctx: MeasureContext, control: TriggerControl, tx: TxCUD): Promise {
+ if (tx._class !== core.class.TxUpdateDoc && tx._class !== core.class.TxMixin) return []
+
+ const hierarchy = control.hierarchy
+
+ const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
+ if (mixin === undefined) return []
+
+ const { objectClass, objectId, objectSpace } = tx
+ const ops = isMixinTx(tx) ? tx.attributes : (tx as TxUpdateDoc).operations
+ const addedCollaborators = await getNewCollaborators(ops, mixin, objectClass, control)
+ const isSpace = control.hierarchy.isDerived(objectClass, core.class.Space)
+ const removedCollaborators = isSpace
+ ? await getRemovedMembers(ops, mixin, objectClass as Ref>, control)
+ : []
+
+ if (removedCollaborators.length === 0 && addedCollaborators.length === 0) return []
+
+ const doc = (await control.findAll(objectClass, { _id: objectId }, { limit: 1 }))[0]
+ if (doc === undefined) return []
+
+ const res: Tx[] = []
+ const currentCollaborators = new Set(hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [])
+ const toAdd = addedCollaborators.filter((p) => !currentCollaborators.has(p))
+
+ if (toAdd.length === 0 && removedCollaborators.length === 0) return []
+
+ if (toAdd.length > 0) {
+ res.push(createPushCollaboratorsTx(control, objectId, objectClass, objectSpace, toAdd))
+ }
+
+ if (removedCollaborators.length > 0) {
+ res.push(createPullCollaboratorsTx(control, objectId, objectClass, objectSpace, removedCollaborators))
+ }
+
+ if (hierarchy.classHierarchyMixin(objectClass, activity.mixin.ActivityDoc) === undefined) return res
+
+ const contexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: objectId })
+ const addedInfo = await getUsersInfo(ctx, toAdd as Ref[], control)
+
+ for (const addedUser of addedInfo) {
+ const info = toReceiverInfo(hierarchy, addedUser)
+ if (info === undefined) continue
+ const context = contexts.find(({ user }) => user === info._id)
+ if (context !== undefined) continue
+ await createNotifyContext(control, objectId, objectClass, objectSpace, info)
+ }
+
+ await removeContexts(contexts, removedCollaborators as Ref[], control)
+
+ return res
+}
+
export async function createCollaboratorNotifications (
ctx: MeasureContext,
tx: TxCUD,
@@ -1418,7 +1599,12 @@ export async function createCollaboratorNotifications (
cache: Map][, Doc> = new Map][, Doc>()
): Promise {
if (tx.space === core.space.DerivedTx) {
- return []
+ // do not forgot update collaborators for derived tx
+ return await ctx.with(
+ 'updateDerivedCollaborators',
+ {},
+ async () => await updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD)
+ )
}
if (activityMessages.length === 0) {
diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts
index d34f1ac3fa..a973aac609 100644
--- a/server-plugins/notification-resources/src/utils.ts
+++ b/server-plugins/notification-resources/src/utils.ts
@@ -12,6 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
+import notification, {
+ BaseNotificationType,
+ Collaborators,
+ CommonNotificationType,
+ NotificationContent,
+ NotificationProvider,
+ NotificationType
+} from '@hcengineering/notification'
+import type { TriggerControl } from '@hcengineering/server-core'
import { Analytics } from '@hcengineering/analytics'
import contact, { formatName, PersonAccount } from '@hcengineering/contact'
import core, {
@@ -23,6 +32,7 @@ import core, {
matchQuery,
MixinUpdate,
Ref,
+ Space,
toIdMap,
Tx,
TxCreateDoc,
@@ -33,15 +43,7 @@ import core, {
TxUpdateDoc,
type MeasureContext
} from '@hcengineering/core'
-import notification, {
- BaseNotificationType,
- CommonNotificationType,
- NotificationContent,
- NotificationProvider,
- NotificationType
-} from '@hcengineering/notification'
import { getResource, IntlString, translate } from '@hcengineering/platform'
-import type { TriggerControl } from '@hcengineering/server-core'
import serverNotification, {
getPersonAccountById,
HTMLPresenter,
@@ -158,7 +160,9 @@ export async function isAllowed (
return false
}
- const typesSettings = await control.queryFind(notification.class.NotificationTypeSetting, {})
+ const typesSettings = await control.queryFind(notification.class.NotificationTypeSetting, {
+ space: core.space.Workspace
+ })
const setting = typesSettings.find(
(it) => it.attachedTo === provider._id && it.type === type._id && it.modifiedBy === receiver
)
@@ -475,6 +479,7 @@ export async function getUsersInfo (
ids: Ref[],
control: TriggerControl
): Promise<(ReceiverInfo | SenderInfo)[]> {
+ if (ids.length === 0) return []
const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: ids } })
const personIds = accounts.map((it) => it.person)
const accountById = toIdMap(accounts)
@@ -522,3 +527,32 @@ export function toReceiverInfo (hierarchy: Hierarchy, info?: SenderInfo | Receiv
space: info.space
}
}
+
+export function createPushCollaboratorsTx (
+ control: TriggerControl,
+ objectId: Ref,
+ objectClass: Ref>,
+ space: Ref,
+ collaborators: Ref[]
+): TxMixin {
+ return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, {
+ $push: {
+ collaborators: {
+ $each: collaborators,
+ $position: 0
+ }
+ }
+ })
+}
+
+export function createPullCollaboratorsTx (
+ control: TriggerControl,
+ objectId: Ref,
+ objectClass: Ref>,
+ space: Ref,
+ collaborators: Ref[]
+): TxMixin {
+ return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, {
+ $pull: { collaborators: { $in: collaborators } }
+ })
+}
diff --git a/tests/sanity/tests/chat/chat.spec.ts b/tests/sanity/tests/chat/chat.spec.ts
index 19eb25affd..c2237960a5 100644
--- a/tests/sanity/tests/chat/chat.spec.ts
+++ b/tests/sanity/tests/chat/chat.spec.ts
@@ -158,10 +158,11 @@ test.describe('channel tests', () => {
await channelPageSecond.checkIfChannelTableExist(data.channelName, true)
await channelPageSecond.clickJoinChannelButton()
await channelPageSecond.clickChooseChannel(data.channelName)
+ const checkJoinButton = await page2.locator('button[data-id="btnJoin"]').isVisible({ timeout: 1500 })
+ if (checkJoinButton) await page2.locator('button[data-id="btnJoin"]').click()
await channelPageSecond.checkMessageExist('Test message', true, 'Test message')
await channelPageSecond.sendMessage('My dream is to fly')
await channelPageSecond.checkMessageExist('My dream is to fly', true, 'My dream is to fly')
- await channelPage.clickOnClosePopupButton()
await channelPage.checkMessageExist('My dream is to fly', true, 'My dream is to fly')
await page2.close()
})
diff --git a/tests/sanity/tests/model/channel-page.ts b/tests/sanity/tests/model/channel-page.ts
index 9991085cc1..d4c898ea3d 100644
--- a/tests/sanity/tests/model/channel-page.ts
+++ b/tests/sanity/tests/model/channel-page.ts
@@ -30,20 +30,20 @@ export class ChannelPage extends CommonPage {
readonly addMemberToChannelButton = (userName: string): Locator => this.page.getByText(userName)
readonly joinChannelButton = (): Locator => this.page.getByRole('button', { name: 'Join' })
readonly addEmojiButton = (): Locator =>
- this.page.locator('.activityMessage-actionPopup > button[data-id$="AddReactionAction"]')
+ this.page.locator('.activityMessage-actionPopup > button[data-id$="AddReactionAction"]').last()
readonly selectEmoji = (emoji: string): Locator => this.page.getByText(emoji)
readonly saveMessageButton = (): Locator =>
- this.page.locator('.activityMessage-actionPopup > button[data-id$="SaveForLaterAction"]')
+ this.page.locator('.activityMessage-actionPopup > button[data-id$="SaveForLaterAction"]').last()
readonly pinMessageButton = (): Locator =>
- this.page.locator('.activityMessage-actionPopup > button[data-id$="PinMessageAction"]')
+ this.page.locator('.activityMessage-actionPopup > button[data-id$="PinMessageAction"]').last()
readonly replyButton = (): Locator =>
- this.page.locator('.activityMessage-actionPopup > button[data-id$="ReplyToThreadAction"]')
+ this.page.locator('.activityMessage-actionPopup > button[data-id$="ReplyToThreadAction"]').last()
readonly openMoreButton = (): Locator =>
- this.page.locator('.activityMessage-actionPopup > button[data-id="btnMoreActions"]')
+ this.page.locator('.activityMessage-actionPopup > button[data-id="btnMoreActions"]').last()
readonly messageSaveMarker = (): Locator => this.page.locator('.saveMarker')
readonly saveMessageTab = (): Locator => this.page.getByRole('button', { name: 'Saved' })
@@ -66,7 +66,7 @@ export class ChannelPage extends CommonPage {
readonly privateOrPublicPopupButton = (change: string): Locator =>
this.page.locator('div.popup div.menu-item', { hasText: change })
- readonly userAdded = (user: string): Locator => this.page.getByText(user)
+ readonly userAdded = (user: string): Locator => this.page.locator('.members').getByText(user)
private readonly addMemberPreview = (): Locator => this.page.getByRole('button', { name: 'Add members' })
private readonly addButtonPreview = (): Locator => this.page.getByRole('button', { name: 'Add', exact: true })
]