UBERF-7717: reduce finds on members changed (#6219)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
Signed-off-by: Alexander Platov <alexander.platov@hardcoreeng.com>
This commit is contained in:
Kristina 2024-08-06 19:32:39 +04:00 committed by GitHub
parent 4e7700c4f2
commit 4e81624688
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 977 additions and 472 deletions

View File

@ -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: '<p><b>{sender}</b> has sent you a message {doc}</p> {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: '<p><b>{sender}</b> has sent a message in {doc}</p> {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: '<p>{body}</p>',
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<ActivityMessageControl<ChunterSpace>>(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<ActivityMessageControl<ChunterSpace>>(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

View File

@ -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<void> {
}
}
async function removeWrongActivity (client: MigrationClient): Promise<void> {
await client.deleteMany<DocUpdateMessage>(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage,
attachedToClass: chunter.class.Channel,
action: 'update',
'attributeUpdates.attrKey': { $ne: 'members' }
})
await client.deleteMany<DocUpdateMessage>(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage,
attachedToClass: chunter.class.Channel,
action: 'create',
objectClass: { $ne: chunter.class.Channel }
})
await client.deleteMany<DocUpdateMessage>(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage,
attachedToClass: chunter.class.Channel,
action: 'remove'
})
await client.deleteMany<DocUpdateMessage>(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage,
attachedToClass: chunter.class.DirectMessage,
action: 'update',
'attributeUpdates.attrKey': { $ne: 'members' }
})
await client.deleteMany<DocUpdateMessage>(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage,
attachedToClass: chunter.class.DirectMessage,
action: 'create'
})
await client.deleteMany<DocUpdateMessage>(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage,
attachedToClass: chunter.class.DirectMessage,
action: 'remove'
})
}
export const chunterOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
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)
}
}
])
},

View File

@ -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: '<p><b>{sender}</b> has sent you a message {doc}</p> {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: '<p><b>{sender}</b> has sent a message in {doc}</p> {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: '<p>You have been added to <b>#{doc}</b></p>',
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: '<p>{body}</p>',
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
})
}

View File

@ -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<Action>,
@ -51,6 +52,10 @@ export default mergeIds(chunterId, chunter, {
category: {
Chunter: '' as Ref<ActionCategory>
},
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<ViewletDescriptor>,

View File

@ -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, {

View File

@ -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, {

View File

@ -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 }

View File

@ -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, {

View File

@ -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: {

View File

@ -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 }

View File

@ -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<Space> | undefined = undefined
export let object: Doc | undefined
export let message: DocUpdateMessage
const client = getClient()
const hierarchy = client.getHierarchy()
@ -41,7 +43,7 @@
</script>
{#if presenter}
<Component is={presenter} props={{ value: attributeUpdates, space }} />
<Component is={presenter} props={{ value: attributeUpdates, space, object, message }} />
{:else}
{#if attributeUpdates.added.length}
<AddedAttributesPresenter {viewlet} {attributeModel} values={attributeUpdates.added} {preview} />

View File

@ -229,7 +229,14 @@
/>
</ShowMore>
{:else if value.attributeUpdates && attributeModel}
<DocUpdateMessageAttributes attributeUpdates={value.attributeUpdates} {attributeModel} {viewlet} {space} />
<DocUpdateMessageAttributes
attributeUpdates={value.attributeUpdates}
{attributeModel}
{viewlet}
{space}
{object}
message={value}
/>
{/if}
</svelte:fragment>
</ActivityMessageTemplate>

View File

@ -130,6 +130,8 @@
{attributeModel}
{space}
{viewlet}
{object}
message={value}
preview
/>
{/if}

View File

@ -67,6 +67,7 @@ export interface ActivityMessageControl<T extends Doc = Doc> extends Doc {
// Skip field activity operations.
skipFields?: (keyof T)[]
allowedFields?: (keyof T)[]
}
/**

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -111,6 +111,14 @@
"JoinChannelText": "Присоединившись, вы сможете читать все сообщения и участвовать в обсуждении.",
"NoMessagesInChannel": "В этом канале пока нет сообщений",
"SendMessagesInChannel": "Отправьте первое сообщение, чтобы начать общение",
"LatestMessages": "↓ Последние сообщения"
"LatestMessages": "↓ Последние сообщения",
"Joined": "Присоединился",
"Left": "Покинул",
"Added": "Добавил(а)",
"Removed": "Исключил(а)",
"CreatedChannelOn": "Создал этот канал {date}",
"ChannelMessages": "Сообщения каналов",
"JoinChannel": "Приссоединение к каналу",
"YouJoinedChannel": "Вы присоединились к каналу"
}
}

View File

@ -111,6 +111,14 @@
"JoinChannelText": "加入后,你将能够阅读所有消息并参与讨论。",
"NoMessagesInChannel": "此频道中没有消息。",
"SendMessagesInChannel": "在此频道中发送消息。",
"LatestMessages": "↓ 最新消息"
"LatestMessages": "↓ 最新消息",
"Joined": "已加入",
"Left": "已离开",
"Added": "已添加",
"Removed": "已移除",
"CreatedChannelOn": "于 {date} 创建此频道",
"ChannelMessages": "频道消息",
"JoinChannel": "加入频道",
"YouJoinedChannel": "你已加入频道"
}
}

View File

@ -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<ActivityMessage[]>,
query?: DocumentQuery<ActivityMessage>
): void {
private loadTail (start?: Timestamp, query?: DocumentQuery<ActivityMessage>): 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<Ref<ChatMessage>> {
getChunkSkipIds (after: Timestamp, loadTail = false): Array<Ref<ActivityMessage>> {
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<Ref<ChatMessage>>
.map(({ _id }) => _id)
}
async loadNext (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise<void> {
@ -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
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import { Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import notification, { DocNotifyContext } from '@hcengineering/notification'
import activity, { ActivityMessage, ActivityMessagesFilter, WithReferences } from '@hcengineering/activity'
import { getClient, isSpace } from '@hcengineering/presentation'
@ -60,16 +60,11 @@
let refsLoaded = false
$: isDocChannel = !hierarchy.isDerived(object._class, chunter.class.ChunterSpace)
$: _class = isDocChannel ? activity.class.ActivityMessage : chunter.class.ChatMessage
$: collection = isDocChannel ? 'comments' : 'messages'
$: void updateDataProvider(object._id, _class, selectedMessageId)
$: void updateDataProvider(object._id, selectedMessageId)
async function updateDataProvider (
attachedTo: Ref<Doc>,
_class: Ref<Class<ActivityMessage>>,
selectedMessageId?: Ref<ActivityMessage>
): Promise<void> {
async function updateDataProvider (attachedTo: Ref<Doc>, selectedMessageId?: Ref<ActivityMessage>): Promise<void> {
if (dataProvider === undefined) {
// For now loading all messages for documents with activity. Need to correct handle aggregation with pagination.
// Perhaps we should load all activity messages once, and keep loading in chunks only for ChatMessages then merge them correctly with activity messages
@ -83,7 +78,15 @@
const hasRefs = ((object as WithReferences<Doc>).references ?? 0) > 0
refsLoaded = hasRefs
const space = isSpace(object) ? object._id : object.space
dataProvider = new ChannelDataProvider(ctx, space, attachedTo, _class, selectedMessageId, loadAll, hasRefs)
dataProvider = new ChannelDataProvider(
ctx,
space,
attachedTo,
activity.class.ActivityMessage,
selectedMessageId,
loadAll,
hasRefs
)
}
}

View File

@ -47,7 +47,7 @@
/>
</div>
<Scroller>
{#each persons as person, index}
{#each persons as person, index (person._id)}
<div class="item" class:withoutBorder={index === persons.length - 1}>
<div class="item__content" class:disabled={disableRemoveFor.includes(person._id)}>
<UserDetails {person} showStatus />

View File

@ -123,7 +123,7 @@
<Label label={chunter.string.JoinChannelText} />
</span>
<span class="mt-4"> </span>
<ModernButton label={view.string.Join} kind="primary" on:click={join} />
<ModernButton label={view.string.Join} kind={'primary'} dataId={'btnJoin'} on:click={join} />
</div>
</div>
{:else}

View File

@ -43,19 +43,19 @@
$: pinnedQuery.query(
activity.class.ActivityMessage,
{ attachedTo: _id, isPinned: true, space: channelSpace },
(res: ActivityMessage[]) => {
pinnedMessagesCount = res.length
(res) => {
pinnedMessagesCount = res.total
},
{ projection: { _id: 1, space: 1, attachedTo: 1, isPinned: 1 } }
{ total: true, limit: 1 }
)
$: pinnedThreadsQuery.query(
chunter.class.ThreadMessage,
{ objectId: _id, isPinned: true, space: channelSpace },
(res: ThreadMessage[]) => {
pinnedThreadsCount = res.length
(res) => {
pinnedThreadsCount = res.total
},
{ projection: { _id: 1, space: 1, objectId: 1, isPinned: 1 } }
{ total: true, limit: 1 }
)
$: if (withRefs) {

View File

@ -0,0 +1,46 @@
<!--
// 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.
-->
<script lang="ts">
import { DocUpdateMessage } from '@hcengineering/activity'
import chunter, { Channel } from '@hcengineering/chunter'
import { Label } from '@hcengineering/ui'
import ChannelIcon from '../ChannelIcon.svelte'
export let message: DocUpdateMessage
export let value: Channel | undefined
$: date = new Date(message.createdOn ?? message.modifiedOn).toLocaleString('default', {
day: '2-digit',
month: 'short',
year: 'numeric'
})
</script>
{#if value}
<span class="text flex-gap-1 overflow-label">
<ChannelIcon {value} size="x-small" />
<Label label={chunter.string.CreatedChannelOn} params={{ date }} />
</span>
{/if}
<style lang="scss">
.text {
display: inline-flex;
align-items: center;
color: var(--global-secondary-TextColor);
}
</style>

View File

@ -0,0 +1,128 @@
<!--
// 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.
-->
<script lang="ts">
import { DocAttributeUpdates, DocUpdateMessage } from '@hcengineering/activity'
import { Person, PersonAccount } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { personAccountByIdStore, personByIdStore, PersonPresenter } from '@hcengineering/contact-resources'
import { ChunterSpace } from '@hcengineering/chunter'
import { Label } from '@hcengineering/ui'
import view from '@hcengineering/view'
import chunter from '../../plugin'
import ChannelIcon from '../ChannelIcon.svelte'
export let message: DocUpdateMessage
export let value: DocAttributeUpdates
export let object: ChunterSpace | undefined
let addedPersons: Person[] = []
let removedPersons: Person[] = []
$: removedPersons = getPersons(value.removed)
$: addedPersons = getPersons(value.added.length > 0 ? value.added : value.set)
function getPersons (accounts: DocAttributeUpdates['removed' | 'added' | 'set']): Person[] {
const persons = new Set<Ref<Person>>()
for (const accountRef of accounts) {
const account = $personAccountByIdStore.get(accountRef as Ref<PersonAccount>)
if (account === undefined) continue
if (persons.has(account.person)) continue
persons.add(account.person)
}
return Array.from(persons)
.map((personRef) => $personByIdStore.get(personRef))
.filter((person): person is Person => person !== undefined)
}
$: account = $personAccountByIdStore.get(message.createdBy as Ref<PersonAccount>)
$: isJoined =
account !== undefined &&
removedPersons.length === 0 &&
addedPersons.length === 1 &&
addedPersons[0]._id === account.person
$: isLeave =
account !== undefined &&
removedPersons.length === 1 &&
addedPersons.length === 0 &&
removedPersons[0]._id === account.person
$: differentActions = addedPersons.length > 0 && removedPersons.length > 0
</script>
<span class="text overflow-label">
{#if addedPersons.length > 0}
<span class="inline-flex flex-gap-1">
{#if isJoined}
<span class="lower">
<Label label={chunter.string.Joined} />
</span>
<span class="inline-flex"><ChannelIcon value={object} size="x-small" /> {object?.name}</span>
{:else}
<Label label={chunter.string.Added} />
{#each addedPersons as person, index}
<PersonPresenter value={person} shouldShowAvatar={false} colorInherit overflowLabel={false} />
{#if index < addedPersons.length - 1}
<span class="separator"> , </span>
{/if}
{/each}
{/if}
</span>
{/if}
{#if differentActions}
<Label label={view.string.And} />
{/if}
{#if removedPersons.length > 0}
<span class="inline-flex flex-gap-1">
{#if isLeave}
<span class="lower">
<Label label={chunter.string.Left} />
</span>
<span class="inline-flex"><ChannelIcon value={object} size="x-small" /> {object?.name}</span>
{:else}
<span class:lower={differentActions}>
<Label label={chunter.string.Removed} />
</span>
{#each removedPersons as person, index}
<PersonPresenter value={person} shouldShowAvatar={false} colorInherit overflowLabel={false} />
{#if index < removedPersons.length - 1}
<span class="separator"> , </span>
{/if}
{/each}
{/if}
</span>
{/if}
</span>
<style lang="scss">
.inline-flex {
display: inline-flex;
align-items: center;
}
.text {
color: var(--global-secondary-TextColor);
}
.separator {
margin-left: -0.25rem;
}
</style>

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import activity from '@hcengineering/activity'
import { Class, Doc, groupByArray, reduceCalls, Ref } from '@hcengineering/core'
import core, { Class, Doc, groupByArray, reduceCalls, Ref } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { IntlString } from '@hcengineering/platform'
@ -75,17 +75,26 @@
const contextsByClass = groupByArray(contexts, ({ objectClass }) => objectClass)
for (const [_class, ctx] of contextsByClass.entries()) {
const isChunterSpace = hierarchy.isDerived(_class, chunter.class.ChunterSpace)
const ids = ctx.map(({ objectId }) => objectId)
const { query, limit } = objectsQueryByClass.get(_class) ?? {
query: createQuery(),
limit: hierarchy.isDerived(_class, chunter.class.ChunterSpace) ? -1 : model.maxSectionItems ?? 5
limit: isChunterSpace ? -1 : model.maxSectionItems ?? 5
}
objectsQueryByClass.set(_class, { query, limit: limit ?? model.maxSectionItems ?? 5 })
query.query(_class, { _id: { $in: limit !== -1 ? ids.slice(0, limit) : ids } }, (res: Doc[]) => {
objectsByClass = objectsByClass.set(_class, { docs: res, total: ids.length })
})
query.query(
_class,
{
_id: { $in: limit !== -1 ? ids.slice(0, limit) : ids },
space: isChunterSpace ? core.space.Space : undefined
},
(res) => {
objectsByClass = objectsByClass.set(_class, { docs: res, total: res.total })
},
{ total: true }
)
}
for (const [classRef, query] of objectsQueryByClass.entries()) {

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import notification, { type DocNotifyContext } from '@hcengineering/notification'
import {
import core, {
generateId,
type Ref,
SortingOrder,
@ -375,7 +375,7 @@ export function loadSavedAttachments (): void {
savedAttachmentsQuery.query(
attachment.class.SavedAttachments,
{},
{ space: core.space.Workspace },
(res) => {
savedAttachmentsStore.set(res.filter(({ $lookup }) => $lookup?.attachedTo !== undefined))
},

View File

@ -0,0 +1,24 @@
<!--
// 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.
-->
<script lang="ts">
import { DisplayDocUpdateMessage } from '@hcengineering/activity'
import { BaseMessagePreview } from '@hcengineering/activity-resources'
import chunter from '../../plugin'
export let message: DisplayDocUpdateMessage
</script>
<BaseMessagePreview intlLabel={chunter.string.YouJoinedChannel} {message} on:click />

View File

@ -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<Resources> => ({
ThreadNotificationPresenter,
ChatAside,
ThreadMessagePreview,
ChatMessagePreview
ChatMessagePreview,
JoinChannelNotificationPresenter
},
activity: {
ChannelCreatedMessage,
MembersChangedMessage
},
function: {
GetDmName: getDmName,

View File

@ -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<Doc>, _class: Ref<Class<D
loc.path[4] = ''
loc.query = {}
loc.path.length = 3
closePanel()
navigate(loc)
}

View File

@ -498,6 +498,7 @@ export async function leaveChannelAction (
}
await leaveChannel(channel, getCurrentAccount()._id)
await client.remove(context)
await resetChunterLocIfEqual(channel._id, channel._class, channel)
}

View File

@ -171,12 +171,19 @@ export default plugin(chunterId, {
UnstarChannel: '' as IntlString,
UnstarConversation: '' as IntlString,
NoMessagesInChannel: '' as IntlString,
SendMessagesInChannel: '' as IntlString
SendMessagesInChannel: '' as IntlString,
Joined: '' as IntlString,
Left: '' as IntlString,
Added: '' as IntlString,
Removed: '' as IntlString,
CreatedChannelOn: '' as IntlString,
YouJoinedChannel: '' as IntlString
},
ids: {
DMNotification: '' as Ref<NotificationType>,
ThreadNotification: '' as Ref<NotificationType>,
ChannelNotification: '' as Ref<NotificationType>,
JoinChannelNotification: '' as Ref<NotificationType>,
ThreadMessageViewlet: '' as Ref<ChatMessageViewlet>
},
app: {

View File

@ -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}
/>
<span class="status">
<Label label={statusLabel} />
@ -116,6 +118,7 @@
{type}
{maxWidth}
{showStatus}
{overflowLabel}
/>
{/if}
{:else if shouldShowPlaceholder}

View File

@ -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 @@
</span>
{/if}
{#if shouldShowName}
<span class="ap-label overflow-label" class:colorInherit class:fs-bold={accent}>
<span class="ap-label" class:overflow-label={overflowLabel} class:colorInherit class:fs-bold={accent}>
{name}
</span>
{/if}
</span>
</DocNavLink>
{:else if type === 'text'}
<span class="overflow-label" use:tooltip={disabled ? undefined : showTooltip}>
<span class:overflow-label={overflowLabel} use:tooltip={disabled ? undefined : showTooltip}>
{name}
</span>
{/if}

View File

@ -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}

View File

@ -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)
})
}

View File

@ -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,

View File

@ -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,

View File

@ -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<Doc>,
objectCache?: DocObjectCache
): Promise<TxCollectionCUD<Doc, DocUpdateMessage>[]> {
const { hierarchy } = control
if (tx.space === core.space.DerivedTx) {
return res
}
const { hierarchy } = control
const etx = TxProcessor.extractTx(tx) as TxCUD<Doc>
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<Doc>, control: TriggerControl): Promise<Tx[]> {
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<DocUpdateMessage>))
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<Ref<Doc>, Doc>)
)
return [...txes, ...notificationTxes]
const result = [...txes, ...notificationTxes]
if (result.length > 0) {
await control.apply(result)
}
return []
}
async function OnDocRemoved (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {

View File

@ -250,14 +250,12 @@ export async function getTxAttributesUpdates (
const hierarchy = control.hierarchy
const filterSet = new Set<string>()
for (const c of controlRules ?? []) {
for (const f of c.skipFields ?? []) {
filterSet.add(f)
}
}
const allowedFields = new Set<string>(controlRules?.flatMap((it) => it.allowedFields ?? []) ?? [])
const skipFields = new Set<string>(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<Doc, Doc>).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)) {

View File

@ -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<Doc>, 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<Doc, Collaborators>, 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<Channel>, control: TriggerControl): Promise<Tx[]> {
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<Doc, Collaborators>, control: TriggerControl): Promise<Tx[]> {
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<DocNotifyContext>, control: Trig
return []
}
async function JoinChannelTypeMatch (originTx: Tx, _: Doc, user: Ref<Account>): Promise<boolean> {
if (originTx.modifiedBy === user) return false
if (originTx._class !== core.class.TxUpdateDoc) return false
const tx = originTx as TxUpdateDoc<Channel>
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
}
})

View File

@ -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<TriggerFunc>,
OnChatMessageRemoved: '' as Resource<TriggerFunc>,
OnChannelMembersChanged: '' as Resource<TriggerFunc>,
ChatNotificationsHandler: '' as Resource<TriggerFunc>,
OnUserStatus: '' as Resource<TriggerFunc>,
OnContextUpdate: '' as Resource<TriggerFunc>
@ -40,6 +39,7 @@ export default plugin(serverChunterId, {
ChannelHTMLPresenter: '' as Resource<Presenter>,
ChannelTextPresenter: '' as Resource<Presenter>,
ChunterNotificationContentProvider: '' as Resource<NotificationContentProvider>,
ChatMessageTextPresenter: '' as Resource<Presenter>
ChatMessageTextPresenter: '' as Resource<Presenter>,
JoinChannelTypeMatch: '' as TypeMatchFunc
}
})

View File

@ -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<Class<Doc>>,
value: any,
field: string,
control: TriggerControl
): Promise<Ref<Account>[] | 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<TxCreateDoc<InboxNotification> | undefined> {
const account = receiver.account
const context = contexts.find((context) => context.user === receiver._id && context.objectId === objectId)
let docNotifyContextId: Ref<DocNotifyContext>
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<Doc>,
objectClass: Ref<Class<Doc>>,
objectSpace: Ref<Space>,
receiver: ReceiverInfo,
updateTimestamp?: Timestamp
): Promise<Ref<DocNotifyContext>> {
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<PersonAccount>[],
control: TriggerControl
): Promise<void> {
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<PersonAccount>[],
@ -763,7 +829,7 @@ export async function createCollabDocInfo (
object: Doc,
activityMessages: ActivityMessage[],
params: NotifyParams,
cache: Map<Ref<Doc>, Doc>
unsubscribe: Ref<PersonAccount>[] = []
): Promise<Tx[]> {
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<Doc>,
control: TriggerControl,
doc: Doc
): Promise<{
added: Ref<Account>[]
removed: Ref<Account>[]
result: Ref<Account>[]
}> {
const { hierarchy } = control
const mixin = hierarchy.classHierarchyMixin<Doc, ClassCollaborators>(
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<Doc>).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<Doc>, control: TriggerCont
async function getNewCollaborators (
ops: DocumentUpdate<Doc> | MixinUpdate<Doc, Doc>,
mixin: ClassCollaborators,
doc: Doc,
docClass: Ref<Class<Doc>>,
control: TriggerControl
): Promise<Ref<Account>[]> {
const newCollaborators = new Set<Ref<Account>>()
@ -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<Space> | MixinUpdate<Space, Space>,
mixin: ClassCollaborators,
docClass: Ref<Class<Space>>,
control: TriggerControl
): Promise<Ref<Account>[]> {
const removedCollaborators: Ref<Account>[] = []
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<Doc> | TxMixin<Doc, Doc>,
@ -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<PersonAccount>[],
collabsInfo.result as Ref<PersonAccount>[],
control,
tx,
originTx,
doc,
activityMessages,
params,
cache
collabsInfo.removed as Ref<PersonAccount>[]
)
)
)
@ -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<Doc>): Promise<Tx[]> {
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<Doc>).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<Class<Space>>, 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<PersonAccount>[], 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<PersonAccount>[], control)
return res
}
export async function createCollaboratorNotifications (
ctx: MeasureContext,
tx: TxCUD<Doc>,
@ -1418,7 +1599,12 @@ export async function createCollaboratorNotifications (
cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
): Promise<Tx[]> {
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<Doc>)
)
}
if (activityMessages.length === 0) {

View File

@ -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<PersonAccount>[],
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<Doc>,
objectClass: Ref<Class<Doc>>,
space: Ref<Space>,
collaborators: Ref<Account>[]
): TxMixin<Doc, Collaborators> {
return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, {
$push: {
collaborators: {
$each: collaborators,
$position: 0
}
}
})
}
export function createPullCollaboratorsTx (
control: TriggerControl,
objectId: Ref<Doc>,
objectClass: Ref<Class<Doc>>,
space: Ref<Space>,
collaborators: Ref<Account>[]
): TxMixin<Doc, Collaborators> {
return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, {
$pull: { collaborators: { $in: collaborators } }
})
}

View File

@ -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()
})

View File

@ -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 })