mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
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:
parent
4e7700c4f2
commit
4e81624688
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
171
models/chunter/src/notifications.ts
Normal file
171
models/chunter/src/notifications.ts
Normal 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
|
||||
})
|
||||
}
|
@ -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>,
|
||||
|
@ -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, {
|
||||
|
@ -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, {
|
||||
|
@ -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 }
|
||||
|
@ -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, {
|
||||
|
@ -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: {
|
||||
|
@ -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 }
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
|
@ -130,6 +130,8 @@
|
||||
{attributeModel}
|
||||
{space}
|
||||
{viewlet}
|
||||
{object}
|
||||
message={value}
|
||||
preview
|
||||
/>
|
||||
{/if}
|
||||
|
@ -67,6 +67,7 @@ export interface ActivityMessageControl<T extends Doc = Doc> extends Doc {
|
||||
|
||||
// Skip field activity operations.
|
||||
skipFields?: (keyof T)[]
|
||||
allowedFields?: (keyof T)[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -111,6 +111,14 @@
|
||||
"JoinChannelText": "Присоединившись, вы сможете читать все сообщения и участвовать в обсуждении.",
|
||||
"NoMessagesInChannel": "В этом канале пока нет сообщений",
|
||||
"SendMessagesInChannel": "Отправьте первое сообщение, чтобы начать общение",
|
||||
"LatestMessages": "↓ Последние сообщения"
|
||||
"LatestMessages": "↓ Последние сообщения",
|
||||
"Joined": "Присоединился",
|
||||
"Left": "Покинул",
|
||||
"Added": "Добавил(а)",
|
||||
"Removed": "Исключил(а)",
|
||||
"CreatedChannelOn": "Создал этот канал {date}",
|
||||
"ChannelMessages": "Сообщения каналов",
|
||||
"JoinChannel": "Приссоединение к каналу",
|
||||
"YouJoinedChannel": "Вы присоединились к каналу"
|
||||
}
|
||||
}
|
@ -111,6 +111,14 @@
|
||||
"JoinChannelText": "加入后,你将能够阅读所有消息并参与讨论。",
|
||||
"NoMessagesInChannel": "此频道中没有消息。",
|
||||
"SendMessagesInChannel": "在此频道中发送消息。",
|
||||
"LatestMessages": "↓ 最新消息"
|
||||
"LatestMessages": "↓ 最新消息",
|
||||
"Joined": "已加入",
|
||||
"Left": "已离开",
|
||||
"Added": "已添加",
|
||||
"Removed": "已移除",
|
||||
"CreatedChannelOn": "于 {date} 创建此频道",
|
||||
"ChannelMessages": "频道消息",
|
||||
"JoinChannel": "加入频道",
|
||||
"YouJoinedChannel": "你已加入频道"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 />
|
||||
|
@ -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}
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
@ -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>
|
@ -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()) {
|
||||
|
@ -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))
|
||||
},
|
||||
|
@ -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 />
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -498,6 +498,7 @@ export async function leaveChannelAction (
|
||||
}
|
||||
|
||||
await leaveChannel(channel, getCurrentAccount()._id)
|
||||
await client.remove(context)
|
||||
await resetChunterLocIfEqual(channel._id, channel._class, channel)
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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[]> {
|
||||
|
@ -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)) {
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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) {
|
||||
|
@ -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 } }
|
||||
})
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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 })
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user