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} + + {/if} + {#if differentActions} + + + 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} /> {/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 })