UBERF-5811: rework backlinks (#4887)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2024-03-07 13:53:07 +07:00
committed by GitHub
parent 049c3125b6
commit 734177f3ea
72 changed files with 1552 additions and 1412 deletions
+45 -24
View File
@@ -32,7 +32,8 @@ import {
type TxViewlet,
type ActivityMessageControl,
type SavedMessage,
type IgnoreActivity
type IgnoreActivity,
type ActivityReference
} from '@hcengineering/activity'
import core, {
DOMAIN_MODEL,
@@ -60,7 +61,8 @@ import {
TypeIntlString,
ArrOf,
TypeTimestamp,
UX
UX,
TypeMarkup
} from '@hcengineering/model'
import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
@@ -146,6 +148,26 @@ export class TDocUpdateMessage extends TActivityMessage implements DocUpdateMess
attributeUpdates?: DocAttributeUpdates
}
@Model(activity.class.ActivityReference, activity.class.ActivityMessage)
export class TActivityReference extends TActivityMessage implements ActivityReference {
// Source document we have reference from, it should be parent document for Comment/Message.
@Prop(TypeRef(core.class.Doc), core.string.Object)
@Index(IndexKind.Indexed)
srcDocId!: Ref<Doc>
@Prop(TypeRef(core.class.Class), core.string.Class)
@Index(IndexKind.Indexed)
srcDocClass!: Ref<Class<Doc>>
// Reference to comment/message in source doc
attachedDocId?: Ref<Doc>
attachedDocClass?: Ref<Class<Doc>>
@Prop(TypeMarkup(), activity.string.Message)
@Index(IndexKind.FullText)
message!: string
}
@Model(activity.class.ActivityInfoMessage, activity.class.ActivityMessage)
export class TActivityInfoMessage extends TActivityMessage implements ActivityInfoMessage {
@Prop(TypeIntlString(), activity.string.Update)
@@ -251,7 +273,8 @@ export function createModel (builder: Builder): void {
TActivityInfoMessage,
TActivityMessageControl,
TSavedMessage,
TIgnoreActivity
TIgnoreActivity,
TActivityReference
)
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, view.mixin.ObjectPresenter, {
@@ -262,6 +285,10 @@ export function createModel (builder: Builder): void {
presenter: activity.component.ActivityInfoMessagePresenter
})
builder.mixin(activity.class.ActivityReference, core.class.Class, view.mixin.ObjectPresenter, {
presenter: activity.component.ActivityReferencePresenter
})
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, view.mixin.LinkProvider, {
encode: activity.function.GetFragment
})
@@ -289,6 +316,12 @@ export function createModel (builder: Builder): void {
filter: activity.filter.PinnedFilter
})
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
label: activity.string.Mentions,
position: 60,
filter: activity.filter.ReferencesFilter
})
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,
@@ -297,23 +330,11 @@ export function createModel (builder: Builder): void {
action: 'create',
component: activity.component.ReactionPresenter,
label: activity.string.Reacted,
onlyWithParent: true,
hideIfRemoved: true
onlyWithParent: true
},
activity.ids.ReactionAddedActivityViewlet
)
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: activity.class.Reaction,
action: 'remove',
hideIfRemoved: true
},
activity.ids.ReactionRemovedActivityViewlet
)
builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['createdBy', 'repliedPersons']
})
@@ -330,6 +351,14 @@ export function createModel (builder: Builder): void {
labelPresenter: activity.component.ActivityMessageNotificationLabel
})
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
messageMatch: {
_class: activity.class.DocUpdateMessage,
objectClass: activity.class.Reaction
},
presenter: activity.component.ReactionNotificationPresenter
})
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
@@ -346,14 +375,6 @@ export function createModel (builder: Builder): void {
},
activity.ids.AddReactionNotification
)
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
messageMatch: {
_class: activity.class.DocUpdateMessage,
objectClass: activity.class.Reaction
},
presenter: activity.component.ReactionNotificationPresenter
})
}
export default activity
+2 -2
View File
@@ -30,11 +30,11 @@ export default mergeIds(activityId, activity, {
filter: {
AttributesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
PinnedFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
AllFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
AllFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
ReferencesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
},
ids: {
ReactionAddedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
ReactionRemovedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
ActivityNotificationGroup: '' as Ref<NotificationGroup>,
AddReactionNotification: '' as Ref<NotificationType>
},
+17 -97
View File
@@ -15,12 +15,10 @@
import activity, { type ActivityMessage } from '@hcengineering/activity'
import {
type Backlink,
type Channel,
chunterId,
type ChunterMessage,
type ChunterMessageExtension,
type Comment,
type DirectMessage,
type Message,
type DirectMessageInput,
@@ -63,17 +61,16 @@ import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-c
import notification from '@hcengineering/model-notification'
import view, { createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import chunter from './plugin'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import { TypeBoolean } from '@hcengineering/model'
import type { IntlString, Resource } from '@hcengineering/platform'
import { TActivityMessage } from '@hcengineering/model-activity'
import chunter from './plugin'
export { chunterId } from '@hcengineering/chunter'
export { chunterOperation } from './migration'
export const DOMAIN_CHUNTER = 'chunter' as Domain
export const DOMAIN_COMMENT = 'comment' as Domain
@Model(chunter.class.ChunterSpace, core.class.Space)
export class TChunterSpace extends TSpace implements ChunterSpace {
@@ -135,30 +132,6 @@ export class TMessage extends TChunterMessage implements Message {
lastReply?: Timestamp
}
@Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT)
@UX(chunter.string.Comment, undefined, 'COM')
export class TComment extends TAttachedDoc implements Comment {
@Prop(TypeMarkup(), chunter.string.Message)
@Index(IndexKind.FullText)
message!: string
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(Collection(activity.class.Reaction), activity.string.Reactions)
reactions?: number
@Prop(TypeBoolean(), chunter.string.PinMessage)
pinned?: boolean
}
@Model(chunter.class.Backlink, chunter.class.Comment)
@UX(chunter.string.Reference, chunter.icon.Chunter)
export class TBacklink extends TComment implements Backlink {
backlinkId!: Ref<Doc>
backlinkClass!: Ref<Class<Doc>>
}
@Mixin(chunter.mixin.DirectMessageInput, core.class.Class)
export class TDirectMessageInput extends TClass implements DirectMessageInput {
component!: AnyComponent
@@ -227,8 +200,6 @@ export function createModel (builder: Builder, options = { addApplication: true
TMessage,
TChunterMessage,
TChunterMessageExtension,
TComment,
TBacklink,
TDirectMessage,
TDirectMessageInput,
TChatMessage,
@@ -442,12 +413,6 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.action.CopyChatMessageLink
)
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
label: chunter.string.FilterBacklinks,
position: 60,
filter: chunter.filter.BacklinksFilter
})
builder.mixin(chunter.class.ChunterMessage, core.class.Class, view.mixin.ClassFilters, {
filters: ['space', '_class']
})
@@ -466,29 +431,6 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.ids.ChunterNotificationGroup
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
label: chunter.string.MentionNotification,
generated: false,
hidden: false,
txClasses: [core.class.TxCreateDoc],
objectClass: chunter.class.Backlink,
group: chunter.ids.ChunterNotificationGroup,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: {
textTemplate: '{sender} mentioned you in {doc} {data}',
htmlTemplate: '<p>{sender}</b> mentioned you in {doc}</p> {data}',
subjectTemplate: 'You were mentioned in {doc}'
}
},
chunter.ids.MentionNotification
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
@@ -546,43 +488,6 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.ids.ThreadNotification
)
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: chunter.class.Backlink,
action: 'create',
component: chunter.component.BacklinkContent,
labelComponent: chunter.activity.BacklinkCreatedLabel,
hideIfRemoved: true
},
chunter.ids.BacklinkCreatedActivityViewlet
)
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: chunter.class.Backlink,
action: 'update',
component: chunter.component.BacklinkContent,
labelComponent: chunter.activity.BacklinkCreatedLabel,
hideIfRemoved: true
},
chunter.ids.BacklinkUpdateActivityViewlet
)
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: chunter.class.Backlink,
action: 'remove',
hideIfRemoved: true
},
chunter.ids.BacklinkRemovedActivityViewlet
)
createAction(builder, {
...viewTemplates.open,
target: chunter.class.Channel,
@@ -679,6 +584,11 @@ export function createModel (builder: Builder, options = { addApplication: true
components: { input: chunter.component.ChatMessageInput }
})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: activity.class.ActivityReference,
components: { input: chunter.component.ChatMessageInput }
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: chunter.class.ChatMessage,
components: [{ kind: 'footer', component: chunter.component.Replies }]
@@ -694,6 +604,11 @@ export function createModel (builder: Builder, options = { addApplication: true
components: [{ kind: 'footer', component: chunter.component.Replies }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: activity.class.ActivityReference,
components: [{ kind: 'footer', component: chunter.component.Replies }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: chunter.class.ChatMessage,
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
@@ -709,6 +624,11 @@ export function createModel (builder: Builder, options = { addApplication: true
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: activity.class.ActivityReference,
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
})
builder.mixin(chunter.class.Channel, core.class.Class, chunter.mixin.ObjectChatPanel, {
ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description']
})
+23 -25
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import core, { type Class, type Doc, type Ref, TxOperations } from '@hcengineering/core'
import core, { type Class, type Doc, type Domain, type Ref, TxOperations } from '@hcengineering/core'
import {
type MigrateOperation,
type MigrationClient,
@@ -21,12 +21,13 @@ import {
tryMigrate
} from '@hcengineering/model'
import { chunterId } from '@hcengineering/chunter'
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import notification from '@hcengineering/notification'
import { DOMAIN_COMMENT } from './index'
import chunter from './plugin'
const DOMAIN_COMMENT = 'comment' as Domain
export async function createDocNotifyContexts (
client: MigrationUpgradeClient,
tx: TxOperations,
@@ -103,29 +104,21 @@ export async function createRandom (client: MigrationUpgradeClient, tx: TxOperat
await createDocNotifyContexts(client, tx, chunter.space.Random, chunter.class.Channel)
}
async function createBacklink (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
_id: chunter.space.Backlinks
})
if (current === undefined) {
await tx.createDoc(
core.class.Space,
core.space.Space,
{
name: 'Backlinks',
description: 'Backlinks',
private: false,
archived: false,
members: []
},
chunter.space.Backlinks
)
}
async function convertCommentsToChatMessages (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_COMMENT,
{ _class: 'chunter:class:Comment' as Ref<Class<Doc>> },
{ _class: chunter.class.ChatMessage }
)
await client.move(DOMAIN_COMMENT, { _class: chunter.class.ChatMessage }, DOMAIN_ACTIVITY)
}
async function convertCommentsToChatMessages (client: MigrationClient): Promise<void> {
await client.update(DOMAIN_COMMENT, { _class: chunter.class.Comment }, { _class: chunter.class.ChatMessage })
await client.move(DOMAIN_COMMENT, { _class: chunter.class.ChatMessage }, DOMAIN_ACTIVITY)
async function removeBacklinks (client: MigrationClient): Promise<void> {
await client.deleteMany(DOMAIN_COMMENT, { _class: 'chunter:class:Backlink' as Ref<Class<Doc>> })
await client.deleteMany(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage,
objectClass: 'chunter:class:Backlink' as Ref<Class<Doc>>
})
}
export const chunterOperation: MigrateOperation = {
@@ -136,11 +129,16 @@ export const chunterOperation: MigrateOperation = {
func: convertCommentsToChatMessages
}
])
await tryMigrate(client, chunterId, [
{
state: 'remove-backlinks',
func: removeBacklinks
}
])
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createGeneral(client, tx)
await createRandom(client, tx)
await createBacklink(tx)
}
}
+3 -13
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import type { ActivityMessage, DocUpdateMessageViewlet, TxViewlet } from '@hcengineering/activity'
import type { ActivityMessage, TxViewlet } from '@hcengineering/activity'
import { chunterId, type Channel } from '@hcengineering/chunter'
import chunter from '@hcengineering/chunter-resources/src/plugin'
import { type Client, type Doc, type Ref } from '@hcengineering/core'
@@ -29,8 +29,6 @@ export default mergeIds(chunterId, chunter, {
DirectMessagePresenter: '' as AnyComponent,
MessagePresenter: '' as AnyComponent,
DmPresenter: '' as AnyComponent,
BacklinkContent: '' as AnyComponent,
BacklinkReference: '' as AnyComponent,
ChannelsPanel: '' as AnyComponent,
Chat: '' as AnyComponent,
ChatMessageNotificationLabel: '' as AnyComponent,
@@ -68,7 +66,6 @@ export default mergeIds(chunterId, chunter, {
PinnedMessages: '' as IntlString,
SavedMessages: '' as IntlString,
Emoji: '' as IntlString,
FilterBacklinks: '' as IntlString,
DM: '' as IntlString,
DMNotification: '' as IntlString,
ConfigLabel: '' as IntlString,
@@ -81,21 +78,15 @@ export default mergeIds(chunterId, chunter, {
},
ids: {
TxCommentCreate: '' as Ref<TxViewlet>,
TxBacklinkCreate: '' as Ref<TxViewlet>,
TxCommentRemove: '' as Ref<TxViewlet>,
TxBacklinkRemove: '' as Ref<TxViewlet>,
TxMessageCreate: '' as Ref<TxViewlet>,
TxChatMessageCreate: '' as Ref<TxViewlet>,
TxChatMessageRemove: '' as Ref<TxViewlet>,
ChunterNotificationGroup: '' as Ref<NotificationGroup>,
BacklinkCreatedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
BacklinkUpdateActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
BacklinkRemovedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>
ChunterNotificationGroup: '' as Ref<NotificationGroup>
},
activity: {
TxCommentCreate: '' as AnyComponent,
TxMessageCreate: '' as AnyComponent,
BacklinkCreatedLabel: '' as AnyComponent
TxMessageCreate: '' as AnyComponent
},
space: {
General: '' as Ref<Channel>,
@@ -111,7 +102,6 @@ export default mergeIds(chunterId, chunter, {
GetThreadLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
},
filter: {
BacklinksFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
}
})
+1
View File
@@ -35,6 +35,7 @@
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/model-activity": "^0.6.0",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/model-core": "^0.6.0",
+12 -8
View File
@@ -8,9 +8,10 @@ import {
type ModelLogger,
tryMigrate
} from '@hcengineering/model'
import { DOMAIN_COMMENT } from '@hcengineering/model-chunter'
import core from '@hcengineering/model-core'
import { DOMAIN_VIEW } from '@hcengineering/model-view'
import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import contact, { DOMAIN_CONTACT, contactId } from './index'
async function createSpace (tx: TxOperations): Promise<void> {
@@ -111,20 +112,20 @@ export const contactOperation: MigrateOperation = {
await client.update(
DOMAIN_TX,
{
'tx.attributes.backlinkClass': 'contact:class:Employee'
'tx.attributes.srcDocClass': 'contact:class:Employee'
},
{
$set: { 'tx.attributes.backlinkClass': contact.mixin.Employee }
$set: { 'tx.attributes.srcDocClass': contact.mixin.Employee }
}
)
await client.update(
DOMAIN_TX,
{
'tx.attributes.backlinkClass': 'contact:class:Employee'
'tx.attributes.srcDocClass': 'contact:class:Employee'
},
{
$set: { 'tx.attributes.backlinkClass': contact.mixin.Employee }
$set: { 'tx.attributes.srcDocClass': contact.mixin.Employee }
}
)
@@ -167,9 +168,12 @@ export const contactOperation: MigrateOperation = {
)
}
await client.update(
DOMAIN_COMMENT,
{ backlinkClass: 'contact:class:Employee' },
{ $set: { backlinkClass: contact.mixin.Employee } }
DOMAIN_ACTIVITY,
{
_class: activity.class.ActivityReference,
srcDocClass: 'contact:class:Employee'
},
{ $set: { srcDocClass: contact.mixin.Employee } }
)
await client.update(
'tags' as Domain,
+3 -3
View File
@@ -99,15 +99,15 @@ export class TDocument extends TAttachedDoc implements Document {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
comments?: number
@Prop(Collection(tags.class.TagReference), document.string.Labels)
labels?: number
@Prop(Collection(chunter.class.Backlink), document.string.Backlinks)
@Prop(Collection(activity.class.ActivityReference), document.string.Backlinks)
@Hidden()
backlinks!: number
references!: number
@Prop(TypeString(), document.string.Icon)
@Hidden()
+50 -13
View File
@@ -71,7 +71,9 @@ import {
type ActivityInboxNotification,
type CommonInboxNotification,
type NotificationContextPresenter,
type ActivityNotificationViewlet
type ActivityNotificationViewlet,
type BaseNotificationType,
type CommonNotificationType
} from '@hcengineering/notification'
import { type Asset, type IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
@@ -99,19 +101,26 @@ export class TNotification extends TAttachedDoc implements Notification {
type!: Ref<NotificationType>
}
@Model(notification.class.NotificationType, core.class.Doc, DOMAIN_MODEL)
export class TNotificationType extends TDoc implements NotificationType {
@Model(notification.class.BaseNotificationType, core.class.Doc, DOMAIN_MODEL)
export class TBaseNotificationType extends TDoc implements BaseNotificationType {
generated!: boolean
label!: IntlString
group!: Ref<NotificationGroup>
txClasses!: Ref<Class<Tx>>[]
providers!: Record<Ref<NotificationProvider>, boolean>
objectClass!: Ref<Class<Doc>>
hidden!: boolean
templates?: NotificationTemplate
}
@Model(notification.class.NotificationType, notification.class.BaseNotificationType)
export class TNotificationType extends TBaseNotificationType implements NotificationType {
txClasses!: Ref<Class<Tx>>[]
objectClass!: Ref<Class<Doc>>
onlyOwn?: boolean
}
@Model(notification.class.CommonNotificationType, notification.class.BaseNotificationType)
export class TCommonNotificationType extends TBaseNotificationType implements CommonNotificationType {}
@Model(notification.class.NotificationGroup, core.class.Doc, DOMAIN_MODEL)
export class TNotificationGroup extends TDoc implements NotificationGroup {
label!: IntlString
@@ -136,7 +145,7 @@ export class TNotificationProvider extends TDoc implements NotificationProvider
@Model(notification.class.NotificationSetting, preference.class.Preference)
export class TNotificationSetting extends TPreference implements NotificationSetting {
declare attachedTo: Ref<TNotificationProvider>
type!: Ref<TNotificationType>
type!: Ref<BaseNotificationType>
enabled!: boolean
}
@@ -245,13 +254,18 @@ export class TActivityInboxNotification extends TInboxNotification implements Ac
@Model(notification.class.CommonInboxNotification, notification.class.InboxNotification)
export class TCommonInboxNotification extends TInboxNotification implements CommonInboxNotification {
header?: IntlString
@Prop(TypeIntlString(), notification.string.Message)
message!: IntlString
@Prop(TypeIntlString(), core.string.String)
header?: IntlString
props!: Record<string, any>
icon!: Asset
iconProps!: Record<string, any>
@Prop(TypeIntlString(), notification.string.Message)
message?: IntlString
@Prop(TypeString(), notification.string.Message)
messageHtml?: string
props?: Record<string, any>
icon?: Asset
iconProps?: Record<string, any>
}
@Model(notification.class.ActivityNotificationViewlet, core.class.Doc, DOMAIN_MODEL)
@@ -279,7 +293,9 @@ export function createModel (builder: Builder): void {
TActivityInboxNotification,
TCommonInboxNotification,
TNotificationContextPresenter,
TActivityNotificationViewlet
TActivityNotificationViewlet,
TBaseNotificationType,
TCommonNotificationType
)
// Temporarily disabled, we should think about it
@@ -599,6 +615,27 @@ export function createModel (builder: Builder): void {
})
defineViewlets(builder)
builder.createDoc(
notification.class.CommonNotificationType,
core.space.Model,
{
label: activity.string.Mentions,
generated: false,
hidden: false,
group: notification.ids.NotificationGroup,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: {
textTemplate: '{sender} mentioned you in {doc} {data}',
htmlTemplate: '<p>{sender}</b> mentioned you in {doc}</p> {data}',
subjectTemplate: 'You were mentioned in {doc}'
}
},
notification.ids.MentionCommonNotificationType
)
}
export function generateClassNotificationTypes (
+2 -2
View File
@@ -30,7 +30,7 @@ import {
TypeString,
UX
} from '@hcengineering/model'
import chunter, { TComment } from '@hcengineering/model-chunter'
import chunter, { TChatMessage } from '@hcengineering/model-chunter'
import core, { TAttachedDoc, TClass } from '@hcengineering/model-core'
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
import view from '@hcengineering/model-view'
@@ -77,7 +77,7 @@ export class TRequest extends TAttachedDoc implements Request {
}
@Mixin(request.mixin.RequestDecisionComment, chunter.class.ChatMessage)
export class TRequestDecisionComment extends TComment implements RequestDecisionComment {}
export class TRequestDecisionComment extends TChatMessage implements RequestDecisionComment {}
@Mixin(request.mixin.RequestPresenter, core.class.Class)
export class TRequestPresenter extends TClass implements RequestPresenter {
+3 -1
View File
@@ -36,6 +36,8 @@
"@hcengineering/platform": "^0.6.9",
"@hcengineering/server-activity": "^0.6.0",
"@hcengineering/server-activity-resources": "^0.6.0",
"@hcengineering/server-core": "^0.6.1"
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/server-notification": "^0.6.1"
}
}
+4
View File
@@ -37,4 +37,8 @@ export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverActivity.trigger.OnDocRemoved
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverActivity.trigger.ReferenceTrigger
})
}
+2 -1
View File
@@ -36,6 +36,7 @@
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/server-notification": "^0.6.1"
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/activity": "^0.6.0"
}
}
-13
View File
@@ -50,10 +50,6 @@ export function createModel (builder: Builder): void {
presenter: serverChunter.function.ChunterNotificationContentProvider
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.BacklinkTrigger
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.ChunterTrigger
})
@@ -86,15 +82,6 @@ export function createModel (builder: Builder): void {
}
})
builder.mixin(
chunter.ids.MentionNotification,
notification.class.NotificationType,
serverNotification.mixin.TypeMatch,
{
func: serverChunter.function.IsMeMentioned
}
)
builder.mixin(chunter.ids.DMNotification, notification.class.NotificationType, serverNotification.mixin.TypeMatch, {
func: serverChunter.function.IsDirectMessage
})
+4 -8
View File
@@ -76,14 +76,6 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.OnBacklinkCreate,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxCreateDoc
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.OnAttributeCreate,
txMatch: {
@@ -99,4 +91,8 @@ export function createModel (builder: Builder): void {
_class: core.class.TxUpdateDoc
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.OnDocRemove
})
}
+5 -1
View File
@@ -33,6 +33,10 @@
"Update": "Update",
"Updated": "Updated",
"UpdatedCollection": "Updated",
"Message": "Message"
"Message": "Message",
"Mentioned": "Mentioned",
"You": "You",
"Mentions": "Mentions",
"MentionedYouIn": "Mentioned you in"
}
}
+5 -1
View File
@@ -33,6 +33,10 @@
"Update": "Обновить",
"Updated": "Обновил(а)",
"UpdatedCollection": "Обновленные",
"Message": "Сообщение"
"Message": "Сообщение",
"Mentioned": "Упомянул(а)",
"You": "Вы",
"Mentions": "Упоминания",
"MentionedYouIn": "Упомянул(а) вас в"
}
}
@@ -75,3 +75,5 @@ export function loadSavedMessages (): void {
}, 50)
}
}
loadSavedMessages()
@@ -227,9 +227,7 @@ export async function combineActivityMessages (
const client = getClient()
const uncombinedMessages = messages.filter((message) => message._class !== activity.class.DocUpdateMessage)
const docUpdateMessages = combineByCreateThreshold(
messages.filter((message): message is DocUpdateMessage => message._class === activity.class.DocUpdateMessage)
)
const docUpdateMessages = combineByCreateThreshold(messages.filter(isDocUpdateMessage))
if (docUpdateMessages.length === 0) {
return sortActivityMessages(uncombinedMessages, sortingOrder)
@@ -472,6 +470,10 @@ function getAttributeUpdatesKey (message: DocUpdateMessage): string {
return [attrKey, attrClass, isMixin].join('-')
}
export function referencesFilter (message: ActivityMessage, _class?: Ref<Doc>): boolean {
return message._class === activity.class.ActivityReference
}
export function attributesFilter (message: ActivityMessage, _class?: Ref<Doc>): boolean {
if (message._class === activity.class.DocUpdateMessage) {
return (message as DocUpdateMessage).objectClass === _class
@@ -548,14 +550,22 @@ export async function getMessageFragment (doc: Doc): Promise<string> {
return `${label}-${doc._id}`
}
export function isReactionMessage (message?: ActivityMessage): message is DocUpdateMessage {
function isDocUpdateMessage (message?: ActivityMessage): message is DocUpdateMessage {
if (message === undefined) {
return false
}
if (message._class !== activity.class.DocUpdateMessage) {
return message._class === activity.class.DocUpdateMessage
}
export function isReactionMessage (message?: ActivityMessage): boolean {
if (message === undefined) {
return false
}
return (message as DocUpdateMessage).objectClass === activity.class.Reaction
if (!isDocUpdateMessage(message)) {
return false
}
return message.objectClass === activity.class.Reaction
}
@@ -22,7 +22,6 @@
import ActivityExtensionComponent from './ActivityExtension.svelte'
import ActivityFilter from './ActivityFilter.svelte'
import { combineActivityMessages } from '../activityMessagesUtils'
import { loadSavedMessages } from '../activity'
export let object: Doc
export let showCommenInput: boolean = true
@@ -50,9 +49,9 @@
const res = activityMessagesQuery.query(
activity.class.ActivityMessage,
{ attachedTo: objectId },
{ attachedTo: objectId, hidden: { $ne: true } },
(result: ActivityMessage[]) => {
combineActivityMessages(result, order).then((messages) => {
void combineActivityMessages(result, order).then((messages) => {
activityMessages = messages
isLoading = false
})
@@ -69,10 +68,6 @@
}
$: void updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending)
onMount(() => {
loadSavedMessages()
})
</script>
<div class="antiSection-header high mt-9" class:invisible={transparent}>
@@ -0,0 +1,156 @@
<!--
// 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 activity, { ActivityReference } from '@hcengineering/activity'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Action, Label } from '@hcengineering/ui'
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { Account, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
import { Person, type PersonAccount } from '@hcengineering/contact'
import view, { ObjectPanel } from '@hcengineering/view'
import { DocNavLink, getDocLinkTitle } from '@hcengineering/view-resources'
import ReferenceContent from './ReferenceContent.svelte'
import ReferenceSrcPresenter from './ReferenceSrcPresenter.svelte'
import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte'
export let value: ActivityReference
export let showNotify: boolean = false
export let isHighlighted: boolean = false
export let isSelected: boolean = false
export let shouldScroll: boolean = false
export let embedded: boolean = false
export let withActions: boolean = true
export let showEmbedded = false
export let hideFooter = false
export let actions: Action[] = []
export let skipLabel = false
export let withFlatActions: boolean = true
export let excludedActions: string[] = []
export let hoverable = true
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const currentAccount = getCurrentAccount() as PersonAccount
const srcDocQuery = createQuery()
const targetDocQuery = createQuery()
let srcDoc: Doc | undefined = undefined
let targetDoc: Doc | undefined = undefined
let srcDocPanel: ObjectPanel | undefined
let targetPanel: ObjectPanel | undefined
let targetTitle: string | undefined = undefined
let person: Person | undefined = undefined
$: person = getPerson(value.createdBy ?? value.modifiedBy, $personAccountByIdStore, $personByIdStore)
$: srcDocQuery.query(value.srcDocClass, { _id: value.srcDocId }, (result) => {
srcDoc = result.shift()
})
$: targetDocQuery.query(value.attachedToClass, { _id: value.attachedTo }, (r) => {
targetDoc = r.shift()
})
$: targetPanel = hierarchy.classHierarchyMixin(value.attachedToClass, view.mixin.ObjectPanel)
$: srcDocPanel = hierarchy.classHierarchyMixin(value.srcDocClass, view.mixin.ObjectPanel)
$: targetDoc !== undefined &&
getDocLinkTitle(client, targetDoc._id, targetDoc._class, targetDoc).then((res) => {
targetTitle = res
})
function getPerson (
_id: Ref<Account>,
accountById: Map<Ref<PersonAccount>, PersonAccount>,
personById: Map<Ref<Person>, Person>
): Person | undefined {
const personAccount = accountById.get(_id as Ref<PersonAccount>)
if (personAccount === undefined) {
return undefined
}
return personById.get(personAccount.person)
}
</script>
<ActivityMessageTemplate
message={value}
{person}
{showNotify}
{isHighlighted}
{isSelected}
{shouldScroll}
{embedded}
{excludedActions}
{withActions}
{showEmbedded}
{hideFooter}
{actions}
{skipLabel}
{withFlatActions}
{hoverable}
{hoverStyles}
{onClick}
{onReply}
>
<svelte:fragment slot="header">
<span class="header">
<span class="text-sm lower ml-1">
<Label label={activity.string.Mentioned} />
</span>
{#if targetDoc}
<DocNavLink object={targetDoc} component={targetPanel?.component ?? view.component.EditDoc} shrink={0}>
<span class="text-sm">
{#if currentAccount.person === targetDoc._id}
<Label label={activity.string.You} />
{:else}
{targetTitle}
{/if}
</span>
</DocNavLink>
{/if}
{#if srcDoc}
<span class="text-sm lower"><Label label={activity.string.In} /></span>
<DocNavLink object={srcDoc} component={srcDocPanel?.component ?? view.component.EditDoc} shrink={0}>
<span class="text-sm">
<ReferenceSrcPresenter
{value}
inline={hierarchy.isDerived(srcDoc._class, activity.class.ActivityMessage)}
/>
</span>
</DocNavLink>
{/if}
</span>
</svelte:fragment>
<svelte:fragment slot="content">
<ReferenceContent {value} />
</svelte:fragment>
</ActivityMessageTemplate>
<style lang="scss">
.header {
gap: var(--global-spacing-1);
}
</style>
@@ -1,5 +1,5 @@
<!--
// Copyright © 2021 Anticrm Platform Contributors.
// 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
@@ -13,22 +13,22 @@
// limitations under the License.
-->
<script lang="ts">
import type { Backlink } from '@hcengineering/chunter'
import { createQuery, MessageViewer } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import activity, { ActivityReference } from '@hcengineering/activity'
import chunter from '../plugin'
export let _id: Ref<Backlink> | undefined = undefined
export let value: Backlink | undefined = undefined
export let _id: Ref<ActivityReference> | undefined = undefined
export let value: ActivityReference | undefined = undefined
const query = createQuery()
$: value === undefined &&
_id &&
query.query(chunter.class.Backlink, { _id }, (res) => {
value = res[0]
$: if (value === undefined && _id !== undefined) {
query.query(activity.class.ActivityReference, { _id }, (res) => {
value = res.shift()
})
} else {
query.unsubscribe()
}
</script>
{#if value}
@@ -1,5 +1,5 @@
<!--
// Copyright © 2021 Anticrm Platform Contributors.
// 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
@@ -13,35 +13,34 @@
// limitations under the License.
-->
<script lang="ts">
import type { Backlink } from '@hcengineering/chunter'
import type { Doc } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { AttributeModel } from '@hcengineering/view'
import { getObjectPresenter } from '@hcengineering/view-resources'
import { ActivityReference } from '@hcengineering/activity'
export let value: Backlink
export let value: ActivityReference
export let inline = true
const client = getClient()
const srcDocQuery = createQuery()
let srcDoc: Doc | undefined
let presenter: AttributeModel | undefined
const docQuery = createQuery()
let doc: Doc | undefined
$: srcDocQuery.query(value.srcDocClass, { _id: value.srcDocId }, (r) => {
srcDoc = r.shift()
})
$: value.backlinkClass != null &&
docQuery.query(value.backlinkClass, { _id: value.backlinkId }, (r) => {
doc = r.shift()
})
$: if (doc !== undefined) {
getObjectPresenter(client, doc._class, { key: '' }).then((p) => {
presenter = p
$: if (srcDoc !== undefined) {
void getObjectPresenter(client, srcDoc._class, { key: '' }).then((result) => {
presenter = result
})
}
</script>
{#if presenter}
<span class="labels-row">
<svelte:component this={presenter.presenter} value={doc} {inline} embedded shouldShowAvatar={false} />
<svelte:component this={presenter.presenter} value={srcDoc} {inline} embedded shouldShowAvatar={false} />
</span>
{/if}
+18 -3
View File
@@ -22,12 +22,21 @@ import ActivityInfoMessagePresenter from './components/activity-message/Activity
import ReactionPresenter from './components/reactions/ReactionPresenter.svelte'
import ReactionNotificationPresenter from './components/reactions/ReactionNotificationPresenter.svelte'
import ActivityMessageNotificationLabel from './components/activity-message/ActivityMessageNotificationLabel.svelte'
import ActivityReferencePresenter from './components/activity-reference/ActivityReferencePresenter.svelte'
import { getMessageFragment, attributesFilter, pinnedFilter, allFilter } from './activityMessagesUtils'
import {
getMessageFragment,
attributesFilter,
pinnedFilter,
allFilter,
referencesFilter
} from './activityMessagesUtils'
import { updateReferences } from './references'
export * from './activity'
export * from './utils'
export * from './activityMessagesUtils'
export * from './references'
export { default as Reactions } from './components/reactions/Reactions.svelte'
export { default as ActivityMessageTemplate } from './components/activity-message/ActivityMessageTemplate.svelte'
@@ -40,6 +49,7 @@ export { default as ActivityMessageHeader } from './components/activity-message/
export { default as AddReactionAction } from './components/reactions/AddReactionAction.svelte'
export { default as ActivityMessageAction } from './components/ActivityMessageAction.svelte'
export { default as ActivityMessagesFilterPopup } from './components/FilterPopup.svelte'
export { default as ActivityReferencePresenter } from './components/activity-reference/ActivityReferencePresenter.svelte'
export default async (): Promise<Resources> => ({
component: {
@@ -49,14 +59,19 @@ export default async (): Promise<Resources> => ({
ReactionPresenter,
ActivityInfoMessagePresenter,
ReactionNotificationPresenter,
ActivityMessageNotificationLabel
ActivityMessageNotificationLabel,
ActivityReferencePresenter
},
filter: {
AttributesFilter: attributesFilter,
PinnedFilter: pinnedFilter,
AllFilter: allFilter
AllFilter: allFilter,
ReferencesFilter: referencesFilter
},
function: {
GetFragment: getMessageFragment
},
backreference: {
Update: updateReferences
}
})
@@ -0,0 +1,169 @@
import core, {
type Class,
type Data,
type Doc,
type DocumentQuery,
type Ref,
type RelatedDocument,
type Space,
type TxOperations
} from '@hcengineering/core'
import activity, { type ActivityReference } from '@hcengineering/activity'
import { type IntlString, translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import contact from '@hcengineering/contact'
async function updateReferencesList (
client: TxOperations,
q: DocumentQuery<ActivityReference>,
references: Array<Data<ActivityReference>>,
space: Ref<Space>
): Promise<void> {
const currentRefs: ActivityReference[] = await client.findAll(activity.class.ActivityReference, q)
// We need to find ones we need to remove, and ones we need to update.
for (const c of currentRefs) {
// Find existing and check if we need to update message.
const pos = references.findIndex(
(ref) => ref.srcDocId === c.srcDocId && ref.srcDocClass === c.srcDocClass && ref.attachedTo === c.attachedTo
)
if (pos !== -1) {
// We need to check and update if required.
const data = references[pos]
if (c.message !== data.message) {
await client.updateCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection, {
message: data.message
})
}
references.splice(pos, 1)
} else {
// We need to remove reference.
await client.removeCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection)
}
}
// Add missing references
for (const ref of references) {
const { attachedTo, attachedToClass, collection, ...adata } = ref
await client.addCollection(activity.class.ActivityReference, space, attachedTo, attachedToClass, collection, adata)
}
}
export async function updateReferences (
source: Doc,
key: string,
target: RelatedDocument[],
msg: IntlString
): Promise<void> {
const client = getClient()
const hierarchy = client.getHierarchy()
const message = await translate(msg, {})
const references: Array<Data<ActivityReference>> = target
.filter((it) => !hierarchy.isDerived(it._class, contact.class.Person))
.map((it) => ({
srcDocId: source._id,
srcDocClass: source._class,
attachedTo: it._id,
attachedToClass: it._class,
message,
collection: key
}))
const query: DocumentQuery<ActivityReference> = { srcDocId: source._id, srcDocClass: source._class, collection: key }
const space: Ref<Space> = hierarchy.isDerived(source._class, core.class.Space)
? (source._id as Ref<Space>)
: source.space
await updateReferencesList(client, query, references, space)
}
function extractReferences (
srcDocId: Ref<Doc>,
srcDocClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
attachedDocClass: Ref<Class<Doc>> | undefined,
kids: NodeListOf<ChildNode>
): Array<Data<ActivityReference>> {
const result: Array<Data<ActivityReference>> = []
const nodes: Array<NodeListOf<ChildNode>> = [kids]
while (true) {
const nds = nodes.shift()
if (nds === undefined) {
break
}
nds.forEach((kid) => {
if (
kid.nodeType === Node.ELEMENT_NODE &&
(kid as HTMLElement).localName === 'span' &&
(kid as HTMLElement).getAttribute('data-type') === 'reference'
) {
const el = kid as HTMLElement
const ato = el.getAttribute('data-id') as Ref<Doc>
const atoClass = el.getAttribute('data-objectclass') as Ref<Class<Doc>>
const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass)
if (e === undefined && ato !== attachedDocId && ato !== srcDocId) {
result.push({
attachedTo: ato,
attachedToClass: atoClass,
collection: 'references',
srcDocId,
srcDocClass,
message: el.parentElement?.innerHTML ?? '',
attachedDocId,
attachedDocClass
})
}
}
nodes.push(kid.childNodes)
})
}
return result
}
/**
* @public
*/
export function getReferences (
srcDocId: Ref<Doc>,
srcDocClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
attachedDocClass: Ref<Class<Doc>> | undefined,
content: string
): Array<Data<ActivityReference>> {
const parser = new DOMParser()
const doc = parser.parseFromString(content, 'text/html')
return extractReferences(
srcDocId,
srcDocClass,
attachedDocId,
attachedDocClass,
doc.childNodes as NodeListOf<HTMLElement>
)
}
/**
* @public
*/
export async function createReferences (
client: TxOperations,
srcDocId: Ref<Doc>,
srcDocClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
attachedDocClass: Ref<Class<Doc>> | undefined,
content: string,
space: Ref<Space>
): Promise<void> {
const hierarchy = client.getHierarchy()
const references = getReferences(srcDocId, srcDocClass, attachedDocId, attachedDocClass, content)
for (const ref of references) {
if (hierarchy.isDerived(ref.attachedToClass, contact.class.Person)) {
continue
}
const { attachedTo, attachedToClass, collection, ...adata } = ref
await client.addCollection(activity.class.ActivityReference, space, attachedTo, attachedToClass, collection, adata)
}
}
+32 -4
View File
@@ -24,6 +24,7 @@ import {
DocumentQuery,
Mixin,
Ref,
type RelatedDocument,
Timestamp,
Tx,
TxCreateDoc,
@@ -180,6 +181,22 @@ export interface DocUpdateMessage extends ActivityMessage {
attributeUpdates?: DocAttributeUpdates
}
export interface ActivityReference extends ActivityMessage {
// A mentioned document
// attachedTo: Ref<Doc>
// attachedToClass: Ref<Class<Doc>>
// Source document we have reference from, it should be parent document for Comment/Message.
srcDocId: Ref<Doc>
srcDocClass: Ref<Class<Doc>>
// Reference to comment/message in source doc
attachedDocId?: Ref<Doc>
attachedDocClass?: Ref<Class<Doc>>
message: string
}
/**
* @public
*/
@@ -310,7 +327,8 @@ export default plugin(activityId, {
ActivityMessagesFilter: '' as Ref<Class<ActivityMessagesFilter>>,
ActivityExtension: '' as Ref<Class<ActivityExtension>>,
Reaction: '' as Ref<Class<Reaction>>,
SavedMessage: '' as Ref<Class<SavedMessage>>
SavedMessage: '' as Ref<Class<SavedMessage>>,
ActivityReference: '' as Ref<Class<ActivityReference>>
},
icon: {
Activity: '' as Asset,
@@ -342,7 +360,11 @@ export default plugin(activityId, {
LastReply: '' as IntlString,
RepliesCount: '' as IntlString,
Reacted: '' as IntlString,
Message: '' as IntlString
Message: '' as IntlString,
Mentioned: '' as IntlString,
You: '' as IntlString,
Mentions: '' as IntlString,
MentionedYouIn: '' as IntlString
},
component: {
Activity: '' as AnyComponent,
@@ -351,9 +373,15 @@ export default plugin(activityId, {
ActivityInfoMessagePresenter: '' as AnyComponent,
ReactionPresenter: '' as AnyComponent,
ReactionNotificationPresenter: '' as AnyComponent,
ActivityMessageNotificationLabel: '' as AnyComponent
ActivityMessageNotificationLabel: '' as AnyComponent,
ActivityReferencePresenter: '' as AnyComponent
},
ids: {
AllFilter: '' as Ref<ActivityMessagesFilter>
AllFilter: '' as Ref<ActivityMessagesFilter>,
MentionNotification: '' as Ref<Doc>
},
backreference: {
// Update list of back references
Update: '' as Resource<(source: Doc, key: string, target: RelatedDocument[], label: IntlString) => Promise<void>>
}
})
@@ -1,47 +0,0 @@
import { type Backlink } from '@hcengineering/chunter'
import { type Data, type DocumentQuery, type TxOperations } from '@hcengineering/core'
import chunter from './plugin'
/**
* @public
*/
export async function updateBacklinksList (
client: TxOperations,
q: DocumentQuery<Backlink>,
backlinks: Array<Data<Backlink>>
): Promise<void> {
const current = await client.findAll(chunter.class.Backlink, q)
// We need to find ones we need to remove, and ones we need to update.
for (const c of current) {
// Find existing and check if we need to update message.
const pos = backlinks.findIndex(
(b) => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass && b.attachedTo === c.attachedTo
)
if (pos !== -1) {
// We need to check and update if required.
const data = backlinks[pos]
if (c.message !== data.message) {
await client.updateCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection, {
message: data.message
})
}
backlinks.splice(pos, 1)
} else {
// We need to remove backlink.
await client.removeCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection)
}
}
// Add missing backlinks
for (const backlink of backlinks) {
const { attachedTo, attachedToClass, collection, ...adata } = backlink
await client.addCollection(
chunter.class.Backlink,
chunter.space.Backlinks,
attachedTo,
attachedToClass,
collection,
adata
)
}
}
@@ -159,7 +159,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
this.metadataQuery.query(
this.msgClass,
{ attachedTo: this.chatId },
{ attachedTo: this.chatId, hidden: { $ne: true } },
(res) => {
this.updatesDates(res)
this.metadataStore.set(res)
@@ -235,6 +235,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
this.msgClass,
{
attachedTo: this.chatId,
hidden: { $ne: true },
...(this.tailStart !== undefined ? { createdOn: { $gte: this.tailStart } } : {})
},
async (res) => {
@@ -287,6 +288,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
chunter.class.ChatMessage,
{
attachedTo: this.chatId,
hidden: { $ne: true },
createdOn: isBackward
? loadEqual
? { $lte: loadAfter }
@@ -1,94 +0,0 @@
<!--
// Copyright © 2023 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 type { Backlink } from '@hcengineering/chunter'
import type { Doc } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import view, { ObjectPanel } from '@hcengineering/view'
import { DocNavLink, getDocLinkTitle } from '@hcengineering/view-resources'
import { Label } from '@hcengineering/ui'
import { getCurrentAccount } from '@hcengineering/core'
import { PersonAccount } from '@hcengineering/contact'
import chunter from '../../plugin'
import BacklinkReference from '../BacklinkReference.svelte'
export let value: Backlink | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const currentAccount = getCurrentAccount() as PersonAccount
let docPanel: ObjectPanel | undefined
let targetPanel: ObjectPanel | undefined
let targetTitle: string | undefined = undefined
const docQuery = createQuery()
const targetQuery = createQuery()
let doc: Doc | undefined
let target: Doc | undefined
$: value?.attachedToClass &&
getDocLinkTitle(client, value.attachedTo, value.attachedToClass, target).then((res) => {
targetTitle = res
})
$: value !== undefined &&
docQuery.query(value.backlinkClass, { _id: value.backlinkId }, (r) => {
doc = r.shift()
})
$: value?.attachedToClass &&
targetQuery.query(value.attachedToClass, { _id: value.attachedTo }, (r) => {
target = r.shift()
})
$: targetPanel =
value?.attachedToClass && hierarchy.classHierarchyMixin(value.attachedToClass, view.mixin.ObjectPanel)
$: docPanel = value?.backlinkClass && hierarchy.classHierarchyMixin(value.backlinkClass, view.mixin.ObjectPanel)
</script>
<span class="text-sm lower">
<Label label={chunter.string.Mentioned} />
</span>
{#if target}
<DocNavLink object={target} component={targetPanel?.component ?? view.component.EditDoc} shrink={0}>
<span class="text-sm">
{#if currentAccount.person === target._id}
<Label label={chunter.string.You} />
{:else}
{targetTitle}
{/if}
</span>
</DocNavLink>
{/if}
{#if doc && value}
<span class="text-sm lower"><Label label={chunter.string.In} /></span>
<DocNavLink object={doc} component={docPanel?.component ?? view.component.EditDoc} shrink={0}>
<span class="text-sm"
><BacklinkReference {value} inline={hierarchy.isDerived(doc._class, chunter.class.ChatMessage)} /></span
>
</DocNavLink>
{/if}
<style lang="scss">
span {
margin-left: 0.25rem;
font-weight: 400;
line-height: 1.25rem;
}
</style>
@@ -21,7 +21,6 @@
import chunter, { ChatMessage } from '@hcengineering/chunter'
import { closeTooltip, Label, Lazy, Spinner, resizeObserver, MiniToggle } from '@hcengineering/ui'
import { ObjectPresenter, DocNavLink } from '@hcengineering/view-resources'
import { loadSavedMessages } from '@hcengineering/activity-resources'
import ChatMessageInput from './ChatMessageInput.svelte'
import ChatMessagePresenter from './ChatMessagePresenter.svelte'
@@ -53,10 +52,6 @@
$: if (isTextMode) {
dispatch('tooltip', { kind: 'popup' })
}
onMount(() => {
loadSavedMessages()
})
</script>
<div class="commentPopup-container">
@@ -28,7 +28,6 @@
import { NavigatorModel, SpecialNavModel } from '@hcengineering/workbench'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { loadSavedMessages } from '@hcengineering/activity-resources'
import { onMount } from 'svelte'
import ChatNavigator from './navigator/ChatNavigator.svelte'
@@ -118,7 +117,6 @@
])
onMount(() => {
loadSavedMessages()
loadSavedAttachments()
})
</script>
@@ -95,7 +95,7 @@ export const chatNavGroupsModel: ChatNavGroupModel[] = [
label: activity.string.Activity,
query: {
attachedToClass: {
$nin: [chunter.class.DirectMessage, chunter.class.Channel, chunter.class.Backlink]
$nin: [chunter.class.DirectMessage, chunter.class.Channel]
}
}
}
@@ -18,7 +18,7 @@
import { Breadcrumbs, IconClose, Label, location as locationStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import { getMessageFromLoc, loadSavedMessages } from '@hcengineering/activity-resources'
import { getMessageFromLoc } from '@hcengineering/activity-resources'
import contact from '@hcengineering/contact'
import chunter from '../../plugin'
@@ -88,10 +88,6 @@
{ label: chunter.string.Thread }
]
}
onMount(() => {
loadSavedMessages()
})
</script>
<div class="popupPanel panel">
+4 -53
View File
@@ -14,25 +14,12 @@
//
import { get, writable } from 'svelte/store'
import chunter, {
type Backlink,
type Channel,
type ChatMessage,
chunterId,
type DirectMessage
} from '@hcengineering/chunter'
import {
type Data,
type Doc,
type DocumentQuery,
getCurrentAccount,
type Ref,
type RelatedDocument
} from '@hcengineering/core'
import { type IntlString, type Resources, translate } from '@hcengineering/platform'
import chunter, { type Channel, type ChatMessage, chunterId, type DirectMessage } from '@hcengineering/chunter'
import { getCurrentAccount, type Ref } from '@hcengineering/core'
import { type Resources } from '@hcengineering/platform'
import { MessageBox, getClient } from '@hcengineering/presentation'
import { closePanel, getCurrentLocation, getLocation, navigate, showPopup } from '@hcengineering/ui'
import activity, { type ActivityMessage, type DocUpdateMessage } from '@hcengineering/activity'
import { type ActivityMessage } from '@hcengineering/activity'
import notification, { type DocNotifyContext, inboxId } from '@hcengineering/notification'
import ChannelPresenter from './components/ChannelPresenter.svelte'
@@ -50,9 +37,6 @@ import EditChannel from './components/EditChannel.svelte'
import ChannelPreview from './components/ChannelPreview.svelte'
import ThreadView from './components/threads/ThreadView.svelte'
import ThreadViewPanel from './components/threads/ThreadViewPanel.svelte'
import BacklinkContent from './components/BacklinkContent.svelte'
import BacklinkReference from './components/BacklinkReference.svelte'
import BacklinkCreatedLabel from './components/activity/BacklinkCreatedLabel.svelte'
import ChatMessagePresenter from './components/chat-message/ChatMessagePresenter.svelte'
import ChatMessageInput from './components/chat-message/ChatMessageInput.svelte'
import ChatMessagesPresenter from './components/chat-message/ChatMessagesPresenter.svelte'
@@ -70,7 +54,6 @@ import ChatAside from './components/chat/ChatAside.svelte'
import Replies from './components/Replies.svelte'
import ReplyToThreadAction from './components/ReplyToThreadAction.svelte'
import { updateBacklinksList } from './backlinks'
import {
ChannelTitleProvider,
DirectTitleProvider,
@@ -186,29 +169,6 @@ export async function chunterBrowserVisible (): Promise<boolean> {
return false
}
async function update (source: Doc, key: string, target: RelatedDocument[], msg: IntlString): Promise<void> {
const message = await translate(msg, {})
const backlinks: Array<Data<Backlink>> = target.map((it) => ({
backlinkId: source._id,
backlinkClass: source._class,
attachedTo: it._id,
attachedToClass: it._class,
message,
collection: key
}))
const q: DocumentQuery<Backlink> = { backlinkId: source._id, backlinkClass: source._class, collection: key }
await updateBacklinksList(getClient(), q, backlinks)
}
export function backlinksFilter (message: ActivityMessage, _class?: Ref<Doc>): boolean {
if (message._class === activity.class.DocUpdateMessage) {
return (message as DocUpdateMessage).objectClass === chunter.class.Backlink
}
return false
}
export function chatMessagesFilter (message: ActivityMessage): boolean {
return message._class === chunter.class.ChatMessage
}
@@ -250,7 +210,6 @@ export async function replyToThread (message: ActivityMessage): Promise<void> {
export default async (): Promise<Resources> => ({
filter: {
BacklinksFilter: backlinksFilter,
ChatMessagesFilter: chatMessagesFilter
},
component: {
@@ -271,8 +230,6 @@ export default async (): Promise<Resources> => ({
EditChannel,
ThreadView,
SavedMessages,
BacklinkContent,
BacklinkReference,
ChatMessagePresenter,
ChatMessageInput,
ChatMessagesPresenter,
@@ -301,9 +258,6 @@ export default async (): Promise<Resources> => ({
GetUnreadThreadsCount: getUnreadThreadsCount,
GetThreadLink: getThreadLink
},
activity: {
BacklinkCreatedLabel
},
actionImpl: {
ArchiveChannel,
UnarchiveChannel,
@@ -311,8 +265,5 @@ export default async (): Promise<Resources> => ({
DeleteChatMessage: deleteChatMessage,
OpenChannel,
UnpinAllChannels
},
backreference: {
Update: update
}
})
+1 -50
View File
@@ -15,17 +15,7 @@
import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity'
import type { Person } from '@hcengineering/contact'
import type {
Account,
AttachedDoc,
Class,
Doc,
Mixin,
Ref,
RelatedDocument,
Space,
Timestamp
} from '@hcengineering/core'
import type { Account, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
import { NotificationType } from '@hcengineering/notification'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
@@ -81,35 +71,6 @@ export interface Message extends ChunterMessage {
lastReply?: Timestamp
}
/**
* @public
* @deprecated use ChatMessage instead
*/
// TODO: remove comment
export interface Comment extends AttachedDoc {
message: string
attachments?: number
reactions?: number
pinned?: boolean
}
/**
* @public
*/
export interface Backlink extends Comment {
// A target document
// attachedTo <- target document we point to
// A target document class
// attachedToClass
// Source document we have reference from, it should be parent document for Comment/Message.
backlinkId: Ref<Doc>
// Source document class
backlinkClass: Ref<Class<Doc>>
// Reference to comment documentId
attachedDocId?: Ref<Doc>
}
/**
* @public
*/
@@ -186,8 +147,6 @@ export default plugin(chunterId, {
Message: '' as Ref<Class<Message>>,
ChunterMessage: '' as Ref<Class<ChunterMessage>>,
ThreadMessage: '' as Ref<Class<ThreadMessage>>,
Backlink: '' as Ref<Class<Backlink>>,
Comment: '' as Ref<Class<Comment>>,
ChunterSpace: '' as Ref<Class<ChunterSpace>>,
Channel: '' as Ref<Class<Channel>>,
DirectMessage: '' as Ref<Class<DirectMessage>>,
@@ -199,9 +158,6 @@ export default plugin(chunterId, {
ChunterMessageExtension: '' as Ref<Mixin<ChunterMessageExtension>>,
ObjectChatPanel: '' as Ref<Mixin<ObjectChatPanel>>
},
space: {
Backlinks: '' as Ref<Space>
},
string: {
Reactions: '' as IntlString,
EditUpdate: '' as IntlString,
@@ -238,7 +194,6 @@ export default plugin(chunterId, {
},
ids: {
DMNotification: '' as Ref<NotificationType>,
MentionNotification: '' as Ref<NotificationType>,
ThreadNotification: '' as Ref<NotificationType>,
ChannelNotification: '' as Ref<NotificationType>,
ThreadMessageViewlet: '' as Ref<ChatMessageViewlet>
@@ -246,10 +201,6 @@ export default plugin(chunterId, {
app: {
Chunter: '' as Ref<Doc>
},
backreference: {
// Update list of back references
Update: '' as Resource<(source: Doc, key: string, target: RelatedDocument[], label: IntlString) => Promise<void>>
},
action: {
DeleteChatMessage: '' as Ref<Action>,
OpenChannel: '' as Ref<Action>
+2 -83
View File
@@ -1,8 +1,8 @@
import { deepEqual } from 'fast-equals'
import core, { Class, Data, Doc, Ref, TxOperations } from '@hcengineering/core'
import core, { Ref, TxOperations } from '@hcengineering/core'
import { PersonAccount } from '@hcengineering/contact'
import chunter, { Backlink, DirectMessage } from '.'
import chunter, { DirectMessage } from '.'
/**
* @public
@@ -28,84 +28,3 @@ export async function getDirectChannel (
members: accIds
})
}
function extractBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
message: string,
kids: NodeListOf<ChildNode>
): Array<Data<Backlink>> {
const result: Array<Data<Backlink>> = []
const nodes: Array<NodeListOf<ChildNode>> = [kids]
while (true) {
const nds = nodes.shift()
if (nds === undefined) {
break
}
nds.forEach((kid) => {
if (
kid.nodeType === Node.ELEMENT_NODE &&
(kid as HTMLElement).localName === 'span' &&
(kid as HTMLElement).getAttribute('data-type') === 'reference'
) {
const el = kid as HTMLElement
const ato = el.getAttribute('data-id') as Ref<Doc>
const atoClass = el.getAttribute('data-objectclass') as Ref<Class<Doc>>
const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass)
if (e === undefined && ato !== attachedDocId && ato !== backlinkId) {
result.push({
attachedTo: ato,
attachedToClass: atoClass,
collection: 'backlinks',
backlinkId,
backlinkClass,
message: el.parentElement?.innerHTML ?? '',
attachedDocId
})
}
}
nodes.push(kid.childNodes)
})
}
return result
}
/**
* @public
*/
export function getBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Array<Data<Backlink>> {
const parser = new DOMParser()
const doc = parser.parseFromString(content, 'text/html')
return extractBacklinks(backlinkId, backlinkClass, attachedDocId, content, doc.childNodes as NodeListOf<HTMLElement>)
}
/**
* @public
*/
export async function createBacklinks (
client: TxOperations,
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Promise<void> {
const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
for (const backlink of backlinks) {
const { attachedTo, attachedToClass, collection, ...adata } = backlink
await client.addCollection(
chunter.class.Backlink,
chunter.space.Backlinks,
attachedTo,
attachedToClass,
collection,
adata
)
}
}
+3
View File
@@ -300,6 +300,9 @@ export const contactPlugin = plugin(contactId, {
ContactName: '' as Ref<TemplateField>,
ContactFirstName: '' as Ref<TemplateField>,
ContactLastName: '' as Ref<TemplateField>
},
ids: {
MentionCommonNotificationType: '' as Ref<Doc>
}
})
+1 -1
View File
@@ -33,7 +33,7 @@
<path d="M18.5,9.8c0.4,0,0.8-0.3,0.8-0.8V7.5c0-0.4-0.3-0.8-0.8-0.8s-0.8,0.3-0.8,0.8V9C17.8,9.4,18.1,9.8,18.5,9.8z"/>
<path d="M13.5,6.8c-0.4,0-0.8,0.3-0.8,0.8V9c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8V7.5C14.2,7.1,13.9,6.8,13.5,6.8z"/>
</symbol>
<symbol id="backlinks" viewBox="0 0 32 32">
<symbol id="references" viewBox="0 0 32 32">
<path d="M29.25 6.75997C28.6926 6.20061 28.0302 5.75679 27.3009 5.45396C26.5716 5.15112 25.7897 4.99524 25 4.99524C24.2103 4.99524 23.4284 5.15112 22.6991 5.45396C22.278 5.62881 21.8792 5.85065 21.5101 6.1146C21.0614 6.43545 21.0693 7.07931 21.4594 7.46935C21.8499 7.85988 22.4827 7.84994 22.9575 7.5679C23.1218 7.47029 23.2933 7.38434 23.4707 7.31086C23.9571 7.10938 24.4785 7.00567 25.005 7.00567C25.5315 7.00567 26.0528 7.10938 26.5393 7.31086C27.0257 7.51235 27.4677 7.80767 27.84 8.17997C28.2123 8.55227 28.5076 8.99425 28.7091 9.48068C28.9106 9.96711 29.0143 10.4885 29.0143 11.015C29.0143 11.5415 28.9106 12.0628 28.7091 12.5493C28.5076 13.0357 28.2123 13.4777 27.84 13.85L19.84 21.85C19.0894 22.6019 18.0709 23.0248 17.0085 23.0257C15.9461 23.0267 14.9269 22.6055 14.175 21.855C13.4231 21.1044 13.0001 20.0859 12.9992 19.0235C12.9983 17.9611 13.4194 16.9419 14.17 16.19L14.8803 15.4746C15.2675 15.0846 15.2659 14.4537 14.8787 14.0637C14.4886 13.6709 13.8519 13.6681 13.4604 14.0596L12.75 14.77C12.1906 15.3274 11.7468 15.9897 11.444 16.7191C11.1411 17.4484 10.9852 18.2303 10.9852 19.02C10.9852 19.8097 11.1411 20.5916 11.444 21.3209C11.7468 22.0502 12.1906 22.7125 12.75 23.27C13.8815 24.387 15.41 25.0092 17 25C17.7927 25.0032 18.5782 24.8494 19.3111 24.5473C20.044 24.2452 20.7098 23.8009 21.27 23.24L29.27 15.24C30.3909 14.1123 31.0184 12.5858 31.0147 10.9958C31.0109 9.40582 30.3762 7.88232 29.25 6.75997Z" />
<path d="M4.18997 24.82C3.81656 24.4483 3.52026 24.0065 3.31807 23.52C3.11589 23.0335 3.01181 22.5118 3.01181 21.985C3.01181 21.4581 3.11589 20.9365 3.31807 20.4499C3.52026 19.9634 3.81656 19.5216 4.18997 19.15L12.19 11.15C12.5616 10.7766 13.0034 10.4803 13.4899 10.2781C13.9765 10.0759 14.4981 9.97181 15.025 9.97181C15.5518 9.97181 16.0735 10.0759 16.56 10.2781C17.0465 10.4803 17.4883 10.7766 17.86 11.15C18.231 11.5246 18.5231 11.9698 18.7189 12.4594C18.9147 12.9489 19.0103 13.4728 19 14C19.003 14.5288 18.9012 15.0529 18.7004 15.5421C18.4995 16.0313 18.2037 16.4758 17.83 16.85L16.4072 18.2929C16.0213 18.6842 16.0289 19.3189 16.4175 19.7075C16.808 20.098 17.4466 20.1034 17.8371 19.7129L19.25 18.3C20.3785 17.1715 21.0124 15.6409 21.0124 14.045C21.0124 12.449 20.3785 10.9185 19.25 9.78997C18.1215 8.66147 16.5909 8.02749 14.995 8.02749C13.399 8.02749 11.8685 8.66147 10.74 9.78997L2.73997 17.79C2.17911 18.3476 1.73401 19.0106 1.43029 19.7408C1.12657 20.471 0.970215 21.2541 0.970215 22.045C0.970215 22.8358 1.12657 23.6189 1.43029 24.3492C1.73401 25.0794 2.17911 25.7424 2.73997 26.3C3.87879 27.4084 5.41087 28.0198 6.99997 28C8.26594 28.0012 9.49169 27.6069 10.5118 26.885C10.964 26.5651 10.961 25.921 10.5693 25.5293C10.1781 25.1381 9.54699 25.1518 9.0717 25.4348C8.90786 25.5324 8.73689 25.6184 8.56 25.6919C8.07349 25.8941 7.55182 25.9981 7.02497 25.9981C6.49812 25.9981 5.97645 25.8941 5.48994 25.6919C5.00342 25.4897 4.56164 25.1934 4.18997 24.82Z" />
</symbol>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+1 -1
View File
@@ -26,7 +26,7 @@ loadMetadata(document.icon, {
Videofile: `${icons}#videofile`,
Library: `${icons}#library`,
Teamspace: `${icons}#teamspace`,
Backlinks: `${icons}#backlinks`,
References: `${icons}#references`,
History: `${icons}#history`,
Star: `${icons}#star`,
Starred: `${icons}#starred`
+2 -1
View File
@@ -37,6 +37,8 @@
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/activity-resources": "^0.6.1",
"@hcengineering/platform": "^0.6.9",
"svelte": "^4.2.5",
"@hcengineering/ui": "^0.6.11",
@@ -50,7 +52,6 @@
"@hcengineering/attachment": "^0.6.9",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/login": "^0.6.8",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/preference": "^0.6.9",
@@ -49,7 +49,7 @@
attachments: 0,
labels: 0,
comments: 0,
backlinks: 0
references: 0
}
const dispatch = createEventDispatcher()
@@ -56,7 +56,7 @@
import DocumentEditor from './DocumentEditor.svelte'
import DocumentPresenter from './DocumentPresenter.svelte'
import DocumentTitle from './DocumentTitle.svelte'
import Backlinks from './sidebar/Backlinks.svelte'
import References from './sidebar/References.svelte'
import History from './sidebar/History.svelte'
export let _id: Ref<Document>
@@ -182,8 +182,8 @@
const aside: ButtonItem[] = [
{
id: 'backlinks',
icon: document.icon.Backlinks
id: 'references',
icon: document.icon.References
},
{
id: 'history',
@@ -339,8 +339,8 @@
</div>
<svelte:fragment slot="aside">
{#if selectedAside === 'backlinks'}
<Backlinks doc={doc._id} />
{#if selectedAside === 'references'}
<References doc={doc._id} />
{:else if selectedAside === 'history'}
<History value={doc} {readonly} />
{/if}
@@ -1,49 +0,0 @@
<!--
//
// Copyright © 2023 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 { Backlink } from '@hcengineering/chunter'
import { MessageViewer } from '@hcengineering/presentation'
import { TimeSince } from '@hcengineering/ui'
import { ObjectPresenter } from '@hcengineering/view-resources'
export let value: Backlink
</script>
<div class="container flex-col flex-gap-2 flex-no-shrink">
<div class="flex-between h-8">
<div class="fs-bold overflow-label">
<ObjectPresenter _class={value.backlinkClass} objectId={value.backlinkId} accent />
</div>
<div class="time">
<TimeSince value={value.createdOn ?? value.modifiedOn} />
</div>
</div>
<div class="flex-no-shrink">
<MessageViewer message={value.message} />
</div>
</div>
<style lang="scss">
.container {
padding: 0.5rem 1rem;
border-bottom: 1px solid var(--theme-divider-color);
}
.time {
font-size: 0.75rem;
color: var(--theme-trans-color);
}
</style>
@@ -15,28 +15,28 @@
//
-->
<script lang="ts">
import chunter, { Backlink } from '@hcengineering/chunter'
import { Ref } from '@hcengineering/core'
import { Document } from '@hcengineering/document'
import { createQuery } from '@hcengineering/presentation'
import { Label, Lazy, Scroller } from '@hcengineering/ui'
import activity, { ActivityReference } from '@hcengineering/activity'
import { ActivityReferencePresenter } from '@hcengineering/activity-resources'
import document from '../../plugin'
import BacklinkView from './BacklinkView.svelte'
export let doc: Ref<Document>
const query = createQuery()
let backlinks: Backlink[] = []
let references: ActivityReference[] = []
$: query.query(
chunter.class.Backlink,
activity.class.ActivityReference,
{
attachedTo: doc
},
(res) => {
backlinks = res
references = res
}
)
</script>
@@ -48,11 +48,11 @@
<div class="divider" />
{#if backlinks.length > 0}
<Scroller>
{#each backlinks as backlink}
{#if references.length > 0}
<Scroller padding="0.75rem 0.25rem">
{#each references as reference}
<Lazy>
<BacklinkView value={backlink} />
<ActivityReferencePresenter value={reference} hoverStyles="filledHover" />
</Lazy>
{/each}
</Scroller>
+1 -1
View File
@@ -47,7 +47,7 @@ export async function createEmptyDocument (
embeddings: 0,
labels: 0,
comments: 0,
backlinks: 0,
references: 0,
...data
}
+2 -2
View File
@@ -43,7 +43,7 @@ export interface Document extends AttachedDoc<Document, 'children', Teamspace>,
comments?: number
embeddings?: number
labels?: number
backlinks?: number
references?: number
}
/**
@@ -105,7 +105,7 @@ const documentPlugin = plugin(documentId, {
Videofile: '' as Asset,
Library: '' as Asset,
Teamspace: '' as Asset,
Backlinks: '' as Asset,
References: '' as Asset,
History: '' as Asset,
Star: '' as Asset,
Starred: '' as Asset
@@ -19,8 +19,8 @@
NotificationProvider,
NotificationSetting,
NotificationStatus,
NotificationType,
Notification as PlatformNotification
Notification as PlatformNotification,
BaseNotificationType
} from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getCurrentLocation, showPanel } from '@hcengineering/ui'
@@ -32,7 +32,10 @@
const providersQuery = createQuery()
let settingsReceived = false
let settings: Map<Ref<NotificationType>, NotificationSetting> = new Map<Ref<NotificationType>, NotificationSetting>()
let settings: Map<Ref<BaseNotificationType>, NotificationSetting> = new Map<
Ref<BaseNotificationType>,
NotificationSetting
>()
let provider: NotificationProvider | undefined
$: enabled = 'Notification' in window && Notification?.permission !== 'denied'
@@ -15,6 +15,7 @@
<script lang="ts">
import { IdMap, Ref, toIdMap } from '@hcengineering/core'
import type {
BaseNotificationType,
NotificationGroup,
NotificationProvider,
NotificationSetting,
@@ -26,18 +27,18 @@
import notification from '../plugin'
export let group: Ref<NotificationGroup>
export let settings: Map<Ref<NotificationType>, NotificationSetting[]>
export let settings: Map<Ref<BaseNotificationType>, NotificationSetting[]>
const client = getClient()
let types: NotificationType[] = []
let typesMap: IdMap<NotificationType> = new Map()
let types: BaseNotificationType[] = []
let typesMap: IdMap<BaseNotificationType> = new Map()
let providers: NotificationProvider[] = []
let providersMap: IdMap<NotificationProvider> = new Map()
load()
void load()
const query = createQuery()
$: query.query(notification.class.NotificationType, { group }, (res) => {
$: query.query(notification.class.BaseNotificationType, { group }, (res) => {
types = res
typesMap = toIdMap(types)
})
@@ -50,8 +51,8 @@
$: column = providers.length + 1
function getStatus (
settings: Map<Ref<NotificationType>, NotificationSetting[]>,
type: Ref<NotificationType>,
settings: Map<Ref<BaseNotificationType>, NotificationSetting[]>,
type: Ref<BaseNotificationType>,
provider: Ref<NotificationProvider>
): boolean {
const setting = getSetting(settings, type, provider)
@@ -63,14 +64,14 @@
return typeValue?.providers?.[provider] ?? false
}
function createHandler (type: Ref<NotificationType>, provider: Ref<NotificationProvider>): (evt: any) => void {
function createHandler (type: Ref<BaseNotificationType>, provider: Ref<NotificationProvider>): (evt: any) => void {
return (evt: any) => {
void change(type, provider, evt.detail)
}
}
async function change (
type: Ref<NotificationType>,
type: Ref<BaseNotificationType>,
provider: Ref<NotificationProvider>,
value: boolean
): Promise<void> {
@@ -89,8 +90,8 @@
}
function getSetting (
map: Map<Ref<NotificationType>, NotificationSetting[]>,
type: Ref<NotificationType>,
map: Map<Ref<BaseNotificationType>, NotificationSetting[]>,
type: Ref<BaseNotificationType>,
provider: Ref<NotificationProvider>
): NotificationSetting | undefined {
const typeMap = map.get(type)
@@ -98,10 +99,15 @@
return typeMap.find((p) => p.attachedTo === provider)
}
function getLabel (type: NotificationType): IntlString {
if (type.attachedToClass !== undefined) {
const isNotificationType = (type: BaseNotificationType): type is NotificationType => {
return type._class === notification.class.NotificationType
}
function getLabel (type: BaseNotificationType): IntlString {
if (isNotificationType(type) && type.attachedToClass !== undefined) {
return notification.string.AddedRemoved
}
return notification.string.Change
}
</script>
@@ -16,10 +16,10 @@
import { createEventDispatcher, onDestroy } from 'svelte'
import { Ref } from '@hcengineering/core'
import type {
BaseNotificationType,
NotificationGroup,
NotificationPreferencesGroup,
NotificationSetting,
NotificationType
NotificationSetting
} from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
@@ -51,7 +51,7 @@
groups = res
})
let settings = new Map<Ref<NotificationType>, NotificationSetting[]>()
let settings = new Map<Ref<BaseNotificationType>, NotificationSetting[]>()
const query = createQuery()
@@ -23,7 +23,7 @@
EmployeePresenter
} from '@hcengineering/contact-resources'
import core, { Doc, getDisplayTime, Ref } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import { IntlString, translate } from '@hcengineering/platform'
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import notification, { CommonInboxNotification } from '@hcengineering/notification'
import { ActionIcon, IconMoreH, Label, showPopup } from '@hcengineering/ui'
@@ -61,13 +61,15 @@
object = result[0]
})
$: void translate(value.message, value.props)
.then((message) => {
content = message
})
.catch((err) => {
content = JSON.stringify(err, null, 2)
})
$: void updateContent(value.message, value.messageHtml)
async function updateContent (message?: IntlString, messageHtml?: string): Promise<void> {
if (messageHtml !== undefined) {
content = messageHtml
} else if (message !== undefined) {
content = await translate(message, value.props)
}
}
function handleActionMenuOpened (): void {
isActionMenuOpened = true
@@ -41,8 +41,7 @@
import { Ref, WithLookup } from '@hcengineering/core'
import { ViewletSelector } from '@hcengineering/view-resources'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { isReactionMessage, loadSavedMessages } from '@hcengineering/activity-resources'
import { onMount } from 'svelte'
import { isReactionMessage } from '@hcengineering/activity-resources'
import { inboxMessagesStore, InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import Filter from '../Filter.svelte'
@@ -256,10 +255,6 @@
{ size: 'auto', minSize: 30, maxSize: 'auto', float: undefined }
])
onMount(() => {
loadSavedMessages()
})
function archiveAll (): void {
showPopup(
MessageBox,
+22 -14
View File
@@ -96,18 +96,24 @@ export interface NotificationContent {
intlParamsNotLocalized?: Record<string, IntlString>
}
export interface BaseNotificationType extends Doc {
label: IntlString
// Is autogenerated
generated: boolean
// allowed to change setting (probably we should show it, but disable toggle??)
hidden: boolean
group: Ref<NotificationGroup>
// allowed providers and default value for it
providers: Record<Ref<NotificationProvider>, boolean>
// templates for email (and browser/push?)
templates?: NotificationTemplate
}
/**
* @public
*/
export interface NotificationType extends Doc {
export interface NotificationType extends BaseNotificationType {
// For show/hide with attributes
attribute?: Ref<AnyAttribute>
// Is autogenerated
generated: boolean
// allowed to to change setting (probably we should show it, but disable toggle??)
hidden: boolean
label: IntlString
group: Ref<NotificationGroup>
txClasses: Ref<Class<Tx>>[]
objectClass: Ref<Class<Doc>>
// not allowed to parent doc
@@ -119,14 +125,12 @@ export interface NotificationType extends Doc {
txMatch?: DocumentQuery<Tx>
// use for space collaborators, not object
spaceSubscribe?: boolean
// allowed providers and default value for it
providers: Record<Ref<NotificationProvider>, boolean>
// templates for email (and browser/push?)
templates?: NotificationTemplate
// when true notification will be created for user which trigger it (default - false)
allowedForAuthor?: boolean
}
export interface CommonNotificationType extends BaseNotificationType {}
/**
* @public
*/
@@ -139,7 +143,7 @@ export interface NotificationProvider extends Doc {
*/
export interface NotificationSetting extends Preference {
attachedTo: Ref<NotificationProvider>
type: Ref<NotificationType>
type: Ref<BaseNotificationType>
enabled: boolean
}
@@ -239,7 +243,8 @@ export interface ActivityInboxNotification extends InboxNotification {
export interface CommonInboxNotification extends InboxNotification {
header?: IntlString
message: IntlString
message?: IntlString
messageHtml?: string
props?: Record<string, any>
icon?: Asset
iconProps?: Record<string, any>
@@ -312,7 +317,9 @@ const notification = plugin(notificationId, {
},
class: {
Notification: '' as Ref<Class<Notification>>,
BaseNotificationType: '' as Ref<Class<BaseNotificationType>>,
NotificationType: '' as Ref<Class<NotificationType>>,
CommonNotificationType: '' as Ref<Class<CommonNotificationType>>,
NotificationProvider: '' as Ref<Class<NotificationProvider>>,
NotificationSetting: '' as Ref<Class<NotificationSetting>>,
DocUpdates: '' as Ref<Class<DocUpdates>>,
@@ -327,7 +334,8 @@ const notification = plugin(notificationId, {
ids: {
NotificationSettings: '' as Ref<Doc>,
NotificationGroup: '' as Ref<NotificationGroup>,
CollaboratoAddNotification: '' as Ref<NotificationType>
CollaboratoAddNotification: '' as Ref<NotificationType>,
MentionCommonNotificationType: '' as Ref<CommonNotificationType>
},
providers: {
PlatformNotification: '' as Ref<NotificationProvider>,
@@ -163,7 +163,7 @@
await descriptionBox.createAttachments()
if (_comment.trim().length > 0) {
await client.addCollection(chunter.class.Comment, _space, doc._id, recruit.class.Applicant, 'comments', {
await client.addCollection(chunter.class.ChatMessage, _space, doc._id, recruit.class.Applicant, 'comments', {
message: _comment
})
}
@@ -14,13 +14,14 @@
-->
<script lang="ts">
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import chunter, { Comment } from '@hcengineering/chunter'
import chunter, { ChatMessage } from '@hcengineering/chunter'
import { PersonAccount } from '@hcengineering/contact'
import { AttachedData, getCurrentAccount, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Request, RequestStatus } from '@hcengineering/request'
import type { RefAction } from '@hcengineering/text-editor'
import { Button } from '@hcengineering/ui'
import request from '../plugin'
import Comments from './icons/Comments.svelte'
import DocFail from './icons/DocFail.svelte'
@@ -55,25 +56,32 @@
let message: string = ''
let attachments: number | undefined = 0
async function onUpdate (event: CustomEvent<AttachedData<Comment>>) {
async function onUpdate (event: CustomEvent<AttachedData<ChatMessage>>) {
message = event.detail.message
attachments = event.detail.attachments
}
async function saveComment (): Promise<void> {
const _id = await client.addCollection(chunter.class.Comment, value.space, value._id, value._class, 'comments', {
message,
attachments
})
const _id = await client.addCollection(
chunter.class.ChatMessage,
value.space,
value._id,
value._class,
'comments',
{
message,
attachments
}
)
await client.createMixin(_id, chunter.class.Comment, value.space, request.mixin.RequestDecisionComment, {})
await client.createMixin(_id, chunter.class.ChatMessage, value.space, request.mixin.RequestDecisionComment, {})
refInput.createAttachments()
loading = false
}
async function comment (): Promise<void> {
await client.addCollection(chunter.class.Comment, value.space, value._id, value._class, 'comments', {
await client.addCollection(chunter.class.ChatMessage, value.space, value._id, value._class, 'comments', {
message,
attachments
})
@@ -73,6 +73,8 @@
import view from '@hcengineering/view'
import { ObjectBox } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte'
import activity from '@hcengineering/activity'
import { activeComponent, activeMilestone, generateIssueShortLink, updateIssueRelation } from '../issues'
import tracker from '../plugin'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
@@ -485,7 +487,7 @@
if (client.getHierarchy().isDerived(relatedTo._class, tracker.class.Issue)) {
await updateIssueRelation(operations, relatedTo as Issue, doc, 'relations', '$push')
} else {
const update = await getResource(chunter.backreference.Update)
const update = await getResource(activity.backreference.Update)
await update(doc, 'relations', [relatedTo], tracker.string.AddedReference)
}
}
@@ -1,10 +1,11 @@
<script lang="ts">
import chunter from '@hcengineering/chunter'
import { Class, Doc, Ref, RelatedDocument } from '@hcengineering/core'
import { getResource, IntlString } from '@hcengineering/platform'
import { createQuery, getClient, ObjectSearchPopup, ObjectSearchResult } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { Action, closePopup, Menu, showPopup } from '@hcengineering/ui'
import activity from '@hcengineering/activity'
import { updateIssueRelation } from '../issues'
import tracker from '../plugin'
@@ -40,7 +41,7 @@
return
}
const update = await getResource(chunter.backreference.Update)
const update = await getResource(activity.backreference.Update)
let docs: RelatedDocument[] = []
let label: IntlString = tracker.string.RemoveRelation
@@ -6,6 +6,8 @@
import { Issue } from '@hcengineering/tracker'
import { Component, Icon, IconClose, navigate } from '@hcengineering/ui'
import view from '@hcengineering/view'
import activity from '@hcengineering/activity'
import { issueLinkFragmentProvider, updateIssueRelation } from '../../issues'
import tracker from '../../plugin'
@@ -43,7 +45,7 @@
await updateIssueRelation(client, issueDoc, value, prop, '$pull')
}
const update = await getResource(chunter.backreference.Update)
const update = await getResource(activity.backreference.Update)
let docs: RelatedDocument[] = []
let label: IntlString = tracker.string.RemoveRelation
@@ -39,6 +39,9 @@
"@hcengineering/platform": "^0.6.9",
"@hcengineering/server-activity": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0"
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/text": "^0.6.1",
"@hcengineering/collaboration": "^0.6.0",
"@hcengineering/contact": "^0.6.20"
}
}
@@ -14,44 +14,47 @@
//
import { Class, Doc, Ref } from '@hcengineering/core'
import { getBacklinks } from '../backlinks'
import { getReferencesData } from '../references'
describe('extractBacklinks', () => {
it('should return no backlinks for empty document', () => {
it('should return no references for empty document', () => {
const content = '<p></p>'
const backlinks = getBacklinks(
'backlinkId' as Ref<Doc>,
'backlinkClass' as Ref<Class<Doc>>,
const references = getReferencesData(
'srcDocId' as Ref<Doc>,
'srcDocClass' as Ref<Class<Doc>>,
'attachedDocId' as Ref<Doc>,
'attachedDocClass' as Ref<Class<Doc>>,
content
)
expect(backlinks).toEqual([])
expect(references).toEqual([])
})
it('should parse single backlink', () => {
const content =
'<p>hello <span class="reference" data-type="reference" data-id="id" data-objectclass="contact:class:Person" data-label="Appleseed John">@Appleseed John</span> </p>'
const backlinks = getBacklinks(
'backlinkId' as Ref<Doc>,
'backlinkClass' as Ref<Class<Doc>>,
const references = getReferencesData(
'srcDocId' as Ref<Doc>,
'srcDocClass' as Ref<Class<Doc>>,
'attachedDocId' as Ref<Doc>,
'attachedDocClass' as Ref<Class<Doc>>,
content
)
expect(backlinks.length).toBe(1)
expect(backlinks).toEqual([
expect(references.length).toBe(1)
expect(references).toEqual([
{
attachedTo: 'id',
attachedToClass: 'contact:class:Person',
collection: 'backlinks',
backlinkId: 'backlinkId',
backlinkClass: 'backlinkClass',
collection: 'references',
srcDocId: 'srcDocId',
srcDocClass: 'srcDocClass',
message:
'hello <span data-type="reference" data-id="id" data-objectclass="contact:class:Person" data-label="Appleseed John" class="reference">@Appleseed John</span>',
attachedDocId: 'attachedDocId'
attachedDocId: 'attachedDocId',
attachedDocClass: 'attachedDocClass'
}
])
})
@@ -60,13 +63,14 @@ describe('extractBacklinks', () => {
const content =
'<p><span class="reference" data-type="reference" data-id="id" data-label="Appleseed John" data-objectclass="contact:class:Person">@Appleseed John</span> <span data-type="reference" class="reference" data-id="id" data-label="Appleseed John" data-objectclass="contact:class:Person">@Appleseed John</span> </p>'
const backlinks = getBacklinks(
'backlinkId' as Ref<Doc>,
'backlinkClass' as Ref<Class<Doc>>,
const references = getReferencesData(
'srcDocId' as Ref<Doc>,
'srcDocClass' as Ref<Class<Doc>>,
'attachedDocId' as Ref<Doc>,
'attachedDocClass' as Ref<Class<Doc>>,
content
)
expect(backlinks.length).toBe(1)
expect(references.length).toBe(1)
})
})
+16 -2
View File
@@ -39,6 +39,7 @@ import {
} from '@hcengineering/server-notification-resources'
import { getDocUpdateAction, getTxAttributesUpdates } from './utils'
import { ReferenceTrigger } from './references'
export async function OnReactionChanged (originTx: Tx, control: TriggerControl): Promise<Tx[]> {
const tx = originTx as TxCollectionCUD<ActivityMessage, Reaction>
@@ -60,14 +61,26 @@ export async function removeReactionNotifications (
control: TriggerControl
): Promise<Tx[]> {
const message = (
await control.findAll(activity.class.ActivityMessage, { objectId: tx.tx.objectId }, { projection: { _id: 1 } })
await control.findAll(
activity.class.ActivityMessage,
{ objectId: tx.tx.objectId },
{ projection: { _id: 1, _class: 1, space: 1 } }
)
)[0]
if (message === undefined) {
return []
}
return await removeDocInboxNotifications(message._id, control)
const res: Tx[] = []
const txes = await removeDocInboxNotifications(message._id, control)
const removeTx = control.txFactory.createTxRemoveDoc(message._class, message.space, message._id)
res.push(removeTx)
res.push(...txes)
return res
}
export async function createReactionNotifications (
@@ -384,6 +397,7 @@ async function OnDocRemoved (originTx: TxCUD<Doc>, control: TriggerControl): Pro
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
ReferenceTrigger,
ActivityMessagesHandler,
OnDocRemoved,
OnReactionChanged
@@ -0,0 +1,558 @@
//
// Copyright © 2023 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 { loadCollaborativeDoc, yDocToBuffer } from '@hcengineering/collaboration'
import core, {
Account,
AttachedDoc,
Class,
CollaborativeDoc,
Data,
Doc,
Hierarchy,
Ref,
Space,
Tx,
TxCollectionCUD,
TxCreateDoc,
TxCUD,
TxFactory,
TxMixin,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc,
Type
} from '@hcengineering/core'
import notification, { CommonInboxNotification } from '@hcengineering/notification'
import { ServerKit, extractReferences, getHTML, parseHTML, yDocContentToNodes } from '@hcengineering/text'
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
import contact, { Person, PersonAccount } from '@hcengineering/contact'
import {
getPushCollaboratorTx,
getCommonNotificationTxes,
isMessageAlreadyNotified,
shouldNotifyCommon
} from '@hcengineering/server-notification-resources'
const extensions = [ServerKit]
export async function getPersonNotificationTxes (
reference: Data<ActivityReference>,
control: TriggerControl,
senderId: Ref<Account>,
space: Ref<Space>,
originTx: TxCUD<Doc>
): Promise<Tx[]> {
if (reference.attachedTo === senderId) {
return []
}
const receiver = (
await control.modelDb.findAll(
contact.class.PersonAccount,
{
person: reference.attachedTo as Ref<Person>
},
{ limit: 1 }
)
)[0]
if (receiver === undefined) {
return []
}
const isAvailable = await isSpaceAvailable(receiver, space, control)
if (!isAvailable) {
return []
}
const res: Tx[] = []
const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiver)
if (collaboratorsTx !== undefined) {
res.push(collaboratorsTx)
}
if (await isReferenceAlreadyNotified(reference, receiver._id, control)) {
return res
}
const doc = (await control.findAll(reference.srcDocClass, { _id: reference.srcDocId }))[0]
if (doc === undefined) {
return res
}
const data: Partial<Data<CommonInboxNotification>> = {
header: activity.string.MentionedYouIn,
messageHtml: reference.message
}
const notifyResult = await shouldNotifyCommon(control, receiver._id, notification.ids.MentionCommonNotificationType)
const texes = await getCommonNotificationTxes(
control,
doc,
data,
receiver._id,
senderId,
reference.srcDocId,
reference.srcDocClass,
space,
originTx.modifiedOn,
notifyResult
)
res.push(...texes)
return res
}
async function isSpaceAvailable (user: PersonAccount, spaceId: Ref<Space>, control: TriggerControl): Promise<boolean> {
const space = (await control.findAll<Space>(core.class.Space, { _id: spaceId }))[0]
if (!space?.private) {
return true
}
return space.members.includes(user._id)
}
async function getCollaboratorsTxes (
reference: Data<ActivityReference>,
control: TriggerControl,
receiver: Account
): Promise<TxMixin<Doc, Doc> | undefined> {
const { hierarchy } = control
if (reference.attachedDocClass === undefined || reference.attachedDocId === undefined) {
return undefined
}
if (!hierarchy.isDerived(reference.attachedDocClass, activity.class.ActivityMessage)) {
return undefined
}
const message = (
await control.findAll<ActivityMessage>(
reference.attachedDocClass,
{
_id: reference.attachedDocId as Ref<ActivityMessage>
},
{ limit: 1 }
)
)[0]
if (message === undefined) {
return undefined
}
return getPushCollaboratorTx(control, receiver._id, message)
}
async function isReferenceAlreadyNotified (
reference: Data<ActivityReference>,
receiver: Ref<Account>,
control: TriggerControl
): Promise<boolean> {
const { hierarchy } = control
if (reference.attachedDocClass === undefined || reference.attachedDocId === undefined) {
return false
}
if (!hierarchy.isDerived(reference.attachedDocClass, activity.class.ActivityMessage)) {
return false
}
return await isMessageAlreadyNotified(reference.attachedDocId as Ref<ActivityMessage>, receiver, control)
}
function isMarkupType (type: Ref<Class<Type<any>>>): boolean {
return type === core.class.TypeMarkup || type === core.class.TypeCollaborativeMarkup
}
function isCollaborativeType (type: Ref<Class<Type<any>>>): boolean {
return type === core.class.TypeCollaborativeDoc
}
async function getCreateReferencesTxes (
control: TriggerControl,
storage: StorageAdapter,
txFactory: TxFactory,
createdDoc: Doc,
srcDocId: Ref<Doc>,
srcDocClass: Ref<Class<Doc>>,
srcDocSpace: Ref<Space>,
originTx: TxCUD<Doc>
): Promise<Tx[]> {
const attachedDocId = createdDoc._id
const attachedDocClass = createdDoc._class
const refs: Data<ActivityReference>[] = []
const attributes = control.hierarchy.getAllAttributes(createdDoc._class)
for (const attr of attributes.values()) {
if (isMarkupType(attr.type._class)) {
const content = (createdDoc as any)[attr.name]?.toString() ?? ''
const attrReferences = getReferencesData(srcDocId, srcDocClass, attachedDocId, attachedDocClass, content)
refs.push(...attrReferences)
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
const collaborativeDoc = (createdDoc as any)[attr.name] as CollaborativeDoc
try {
const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx)
if (ydoc !== undefined) {
const attrReferences = getReferencesData(
srcDocId,
srcDocClass,
attachedDocId,
attachedDocClass,
yDocToBuffer(ydoc)
)
refs.push(...attrReferences)
}
} catch {
// do nothing, the collaborative doc does not sem to exist yet
}
}
}
const refSpace: Ref<Space> = control.hierarchy.isDerived(srcDocClass, core.class.Space)
? (srcDocId as Ref<Space>)
: srcDocSpace
return await getReferencesTxes(control, txFactory, refs, refSpace, [], originTx)
}
async function getUpdateReferencesTxes (
control: TriggerControl,
storage: StorageAdapter,
txFactory: TxFactory,
updatedDoc: Doc,
srcDocId: Ref<Doc>,
srcDocClass: Ref<Class<Doc>>,
srcDocSpace: Ref<Space>,
originTx: TxCUD<Doc>
): Promise<Tx[]> {
const attachedDocId = updatedDoc._id
const attachedDocClass = updatedDoc._class
// collect attribute references
let hasReferenceAttrs = false
const references: Data<ActivityReference>[] = []
const attributes = control.hierarchy.getAllAttributes(updatedDoc._class)
for (const attr of attributes.values()) {
if (isMarkupType(attr.type._class)) {
hasReferenceAttrs = true
const content = (updatedDoc as any)[attr.name]?.toString() ?? ''
const attrReferences = getReferencesData(srcDocId, srcDocClass, attachedDocId, attachedDocClass, content)
references.push(...attrReferences)
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
hasReferenceAttrs = true
try {
const collaborativeDoc = (updatedDoc as any)[attr.name] as CollaborativeDoc
const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx)
if (ydoc !== undefined) {
const attrReferences = getReferencesData(
srcDocId,
srcDocClass,
attachedDocId,
attachedDocClass,
yDocToBuffer(ydoc)
)
references.push(...attrReferences)
}
} catch {
// do nothing, the collaborative doc does not sem to exist yet
}
}
}
// There is a chance that references are managed manually
// do not update references if there are no reference sources in the doc
if (hasReferenceAttrs) {
const current = await control.findAll(activity.class.ActivityReference, {
srcDocId,
srcDocClass,
attachedDocId,
collection: 'references'
})
const refSpace: Ref<Space> = control.hierarchy.isDerived(srcDocClass, core.class.Space)
? (srcDocId as Ref<Space>)
: srcDocSpace
return await getReferencesTxes(control, txFactory, references, refSpace, current, originTx)
}
return []
}
export function getReferencesData (
srcDocId: Ref<Doc>,
srcDocClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
attachedDocClass: Ref<Class<Doc>> | undefined,
content: string | Buffer
): Array<Data<ActivityReference>> {
const result: Array<Data<ActivityReference>> = []
const references = []
if (content instanceof Buffer) {
const nodes = yDocContentToNodes(extensions, content)
for (const node of nodes) {
references.push(...extractReferences(node))
}
} else {
const doc = parseHTML(content, extensions)
references.push(...extractReferences(doc))
}
for (const ref of references) {
if (ref.objectId !== attachedDocId && ref.objectId !== srcDocId) {
result.push({
attachedTo: ref.objectId,
attachedToClass: ref.objectClass,
collection: 'references',
srcDocId,
srcDocClass,
message: ref.parentNode !== null ? getHTML(ref.parentNode, extensions) : '',
attachedDocId,
attachedDocClass
})
}
}
return result
}
async function createReferenceTxes (
control: TriggerControl,
txFactory: TxFactory,
ref: Data<ActivityReference>,
space: Ref<Space>,
originTx: TxCUD<Doc>
): Promise<Tx[]> {
if (control.hierarchy.isDerived(ref.attachedToClass, contact.class.Person)) {
return await getPersonNotificationTxes(ref, control, txFactory.account, space, originTx)
}
const refTx = control.txFactory.createTxCreateDoc(activity.class.ActivityReference, space, ref)
const tx = control.txFactory.createTxCollectionCUD(ref.attachedToClass, ref.attachedTo, space, ref.collection, refTx)
return [tx]
}
async function getReferencesTxes (
control: TriggerControl,
txFactory: TxFactory,
references: Data<ActivityReference>[],
space: Ref<Space>,
current: ActivityReference[],
originTx: TxCUD<Doc>
): Promise<Tx[]> {
const txes: Tx[] = []
for (const c of current) {
// Find existing and check if we need to update message
const pos = references.findIndex(
(b) => b.srcDocId === c.srcDocId && b.srcDocClass === c.srcDocClass && b.attachedTo === c.attachedTo
)
if (pos !== -1) {
// Update existing references when message changed
const data = references[pos]
if (c.message !== data.message) {
const innerTx = txFactory.createTxUpdateDoc(c._class, c.space, c._id, {
message: data.message
})
txes.push(txFactory.createTxCollectionCUD(c.attachedToClass, c.attachedTo, c.space, c.collection, innerTx))
}
references.splice(pos, 1)
} else {
// Remove not found references
const innerTx = txFactory.createTxRemoveDoc(c._class, c.space, c._id)
txes.push(txFactory.createTxCollectionCUD(c.attachedToClass, c.attachedTo, c.space, c.collection, innerTx))
}
}
// Add missing references
for (const ref of references) {
txes.push(...(await createReferenceTxes(control, txFactory, ref, space, originTx)))
}
return txes
}
async function getRemoveActivityReferenceTxes (
control: TriggerControl,
txFactory: TxFactory,
removedDocId: Ref<Doc>
): Promise<Tx[]> {
const txes: Tx[] = []
const refs = await control.findAll(activity.class.ActivityReference, {
attachedDocId: removedDocId,
collection: 'references'
})
for (const ref of refs) {
const removeTx = txFactory.createTxRemoveDoc(ref._class, ref.space, ref._id)
txes.push(txFactory.createTxCollectionCUD(ref.attachedToClass, ref.attachedTo, ref.space, ref.collection, removeTx))
}
return txes
}
function guessReferenceTx (hierarchy: Hierarchy, tx: TxCUD<Doc>): TxCUD<Doc> {
// Try to guess reference target Tx for TxCollectionCUD txes based on collaborators availability
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
const cltx = tx as TxCollectionCUD<Doc, AttachedDoc>
tx = TxProcessor.extractTx(cltx) as TxCUD<Doc>
if (hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) {
return cltx
}
const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
return mixin !== undefined ? tx : cltx
}
return tx
}
async function ActivityReferenceCreate (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
if (ctx._class !== core.class.TxCreateDoc) return []
if (control.hierarchy.isDerived(ctx.objectClass, activity.class.ActivityReference)) return []
control.storageFx(async (adapter) => {
const txFactory = new TxFactory(control.txFactory.account)
const doc = TxProcessor.createDoc2Doc(ctx)
const targetTx = guessReferenceTx(control.hierarchy, tx)
const txes: Tx[] = await getCreateReferencesTxes(
control,
adapter,
txFactory,
doc,
targetTx.objectId,
targetTx.objectClass,
targetTx.objectSpace,
tx
)
if (txes.length !== 0) {
await control.apply(txes, true)
}
})
return []
}
async function ActivityReferenceUpdate (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Doc>
const attributes = control.hierarchy.getAllAttributes(ctx.objectClass)
let hasUpdates = false
for (const attr of attributes.values()) {
if (isMarkupType(attr.type._class) || isCollaborativeType(attr.type._class)) {
if (TxProcessor.txHasUpdate(ctx, attr.name)) {
hasUpdates = true
break
}
}
}
if (!hasUpdates) {
return []
}
const rawDoc = (await control.findAll(ctx.objectClass, { _id: ctx.objectId }))[0]
if (rawDoc === undefined) {
return []
}
control.storageFx(async (adapter) => {
const txFactory = new TxFactory(control.txFactory.account)
const doc = TxProcessor.updateDoc2Doc(rawDoc, ctx)
const targetTx = guessReferenceTx(control.hierarchy, tx)
const txes: Tx[] = await getUpdateReferencesTxes(
control,
adapter,
txFactory,
doc,
targetTx.objectId,
targetTx.objectClass,
targetTx.objectSpace,
tx
)
if (txes.length !== 0) {
await control.apply(txes, true)
}
})
return []
}
async function ActivityReferenceRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc<Doc>
const attributes = control.hierarchy.getAllAttributes(ctx.objectClass)
let hasMarkdown = false
for (const attr of attributes.values()) {
if (isMarkupType(attr.type._class)) {
hasMarkdown = true
break
}
}
if (hasMarkdown) {
const txFactory = new TxFactory(control.txFactory.account)
const txes: Tx[] = await getRemoveActivityReferenceTxes(control, txFactory, ctx.objectId)
if (txes.length !== 0) {
await control.apply(txes, true)
}
}
return []
}
/**
* @public
*/
export async function ReferenceTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
const etx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
if (control.hierarchy.isDerived(etx.objectClass, activity.class.ActivityReference)) return []
if (etx._class === core.class.TxCreateDoc) {
result.push(...(await ActivityReferenceCreate(tx, control)))
}
if (etx._class === core.class.TxUpdateDoc) {
result.push(...(await ActivityReferenceUpdate(tx, control)))
}
if (etx._class === core.class.TxRemoveDoc) {
result.push(...(await ActivityReferenceRemove(tx, control)))
}
return result
}
+2 -1
View File
@@ -31,6 +31,7 @@ export default plugin(serverActivityId, {
trigger: {
ActivityMessagesHandler: '' as Resource<TriggerFunc>,
OnDocRemoved: '' as Resource<TriggerFunc>,
OnReactionChanged: '' as Resource<TriggerFunc>
OnReactionChanged: '' as Resource<TriggerFunc>,
ReferenceTrigger: '' as Resource<TriggerFunc>
}
})
@@ -1,251 +0,0 @@
//
// Copyright © 2023 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 chunter, { Backlink } from '@hcengineering/chunter'
import { loadCollaborativeDoc, yDocToBuffer } from '@hcengineering/collaboration'
import core, {
AttachedDoc,
Class,
CollaborativeDoc,
Data,
Doc,
Hierarchy,
Ref,
Tx,
TxCollectionCUD,
TxCUD,
TxFactory,
TxProcessor,
Type
} from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { ServerKit, extractReferences, getHTML, parseHTML, yDocContentToNodes } from '@hcengineering/text'
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
const extensions = [ServerKit]
export function isMarkupType (type: Ref<Class<Type<any>>>): boolean {
return type === core.class.TypeMarkup || type === core.class.TypeCollaborativeMarkup
}
export function isCollaborativeType (type: Ref<Class<Type<any>>>): boolean {
return type === core.class.TypeCollaborativeDoc
}
export async function getCreateBacklinksTxes (
control: TriggerControl,
storage: StorageAdapter,
txFactory: TxFactory,
doc: Doc,
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>
): Promise<Tx[]> {
const attachedDocId = doc._id
const backlinks: Data<Backlink>[] = []
const attributes = control.hierarchy.getAllAttributes(doc._class)
for (const attr of attributes.values()) {
if (isMarkupType(attr.type._class)) {
const content = (doc as any)[attr.name]?.toString() ?? ''
const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
backlinks.push(...attrBacklinks)
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
const collaborativeDoc = (doc as any)[attr.name] as CollaborativeDoc
try {
const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx)
if (ydoc !== undefined) {
const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, yDocToBuffer(ydoc))
backlinks.push(...attrBacklinks)
}
} catch {
// do nothing, the collaborative doc does not sem to exist yet
}
}
}
return getBacklinksTxes(txFactory, backlinks, [])
}
export async function getUpdateBacklinksTxes (
control: TriggerControl,
storage: StorageAdapter,
txFactory: TxFactory,
doc: Doc,
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>
): Promise<Tx[]> {
const attachedDocId = doc._id
// collect attribute backlinks
let hasBacklinkAttrs = false
const backlinks: Data<Backlink>[] = []
const attributes = control.hierarchy.getAllAttributes(doc._class)
for (const attr of attributes.values()) {
if (isMarkupType(attr.type._class)) {
hasBacklinkAttrs = true
const content = (doc as any)[attr.name]?.toString() ?? ''
const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
backlinks.push(...attrBacklinks)
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
hasBacklinkAttrs = true
try {
const collaborativeDoc = (doc as any)[attr.name] as CollaborativeDoc
const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx)
if (ydoc !== undefined) {
const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, yDocToBuffer(ydoc))
backlinks.push(...attrBacklinks)
}
} catch {
// do nothing, the collaborative doc does not sem to exist yet
}
}
}
// There is a chance that backlinks are managed manually
// do not update backlinks if there are no backlink sources in the doc
if (hasBacklinkAttrs) {
const current = await control.findAll(chunter.class.Backlink, {
backlinkId,
backlinkClass,
attachedDocId,
collection: 'backlinks'
})
return getBacklinksTxes(txFactory, backlinks, current)
}
return []
}
export function getBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string | Buffer
): Array<Data<Backlink>> {
const result: Array<Data<Backlink>> = []
const references = []
if (content instanceof Buffer) {
const nodes = yDocContentToNodes(extensions, content)
for (const node of nodes) {
references.push(...extractReferences(node))
}
} else {
const doc = parseHTML(content, extensions)
references.push(...extractReferences(doc))
}
for (const ref of references) {
if (ref.objectId !== attachedDocId && ref.objectId !== backlinkId) {
result.push({
attachedTo: ref.objectId,
attachedToClass: ref.objectClass,
collection: 'backlinks',
backlinkId,
backlinkClass,
message: ref.parentNode !== null ? getHTML(ref.parentNode, extensions) : '',
attachedDocId
})
}
}
return result
}
/**
* @public
*/
export function getBacklinksTxes (txFactory: TxFactory, backlinks: Data<Backlink>[], current: Backlink[]): Tx[] {
const txes: Tx[] = []
for (const c of current) {
// Find existing and check if we need to update message
const pos = backlinks.findIndex(
(b) => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass && b.attachedTo === c.attachedTo
)
if (pos !== -1) {
// Update existing backlinks when message changed
const data = backlinks[pos]
if (c.message !== data.message) {
const innerTx = txFactory.createTxUpdateDoc(c._class, c.space, c._id, {
message: data.message
})
txes.push(
txFactory.createTxCollectionCUD(
c.attachedToClass,
c.attachedTo,
chunter.space.Backlinks,
c.collection,
innerTx
)
)
}
backlinks.splice(pos, 1)
} else {
// Remove not found backlinks
const innerTx = txFactory.createTxRemoveDoc(c._class, c.space, c._id)
txes.push(
txFactory.createTxCollectionCUD(c.attachedToClass, c.attachedTo, chunter.space.Backlinks, c.collection, innerTx)
)
}
}
// Add missing backlinks
for (const backlink of backlinks) {
const backlinkTx = txFactory.createTxCreateDoc(chunter.class.Backlink, chunter.space.Backlinks, backlink)
txes.push(
txFactory.createTxCollectionCUD(
backlink.attachedToClass,
backlink.attachedTo,
chunter.space.Backlinks,
backlink.collection,
backlinkTx
)
)
}
return txes
}
export async function getRemoveBacklinksTxes (
control: TriggerControl,
txFactory: TxFactory,
doc: Ref<Doc>
): Promise<Tx[]> {
const txes: Tx[] = []
const backlinks = await control.findAll(chunter.class.Backlink, { attachedDocId: doc, collection: 'backlinks' })
for (const b of backlinks) {
const innerTx = txFactory.createTxRemoveDoc(b._class, b.space, b._id)
txes.push(
txFactory.createTxCollectionCUD(b.attachedToClass, b.attachedTo, chunter.space.Backlinks, b.collection, innerTx)
)
}
return txes
}
export function guessBacklinkTx (hierarchy: Hierarchy, tx: TxCUD<Doc>): TxCUD<Doc> {
// Try to guess backlink target Tx for TxCollectionCUD txes based on collaborators availability
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
const cltx = tx as TxCollectionCUD<Doc, AttachedDoc>
tx = TxProcessor.extractTx(cltx) as TxCUD<Doc>
const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
return mixin !== undefined ? tx : cltx
}
return tx
}
+8 -133
View File
@@ -14,7 +14,6 @@
//
import chunter, {
Backlink,
Channel,
ChatMessage,
chunterId,
@@ -37,7 +36,6 @@ import core, {
TxCollectionCUD,
TxCreateDoc,
TxCUD,
TxFactory,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc
@@ -53,17 +51,9 @@ import {
import { workbenchId } from '@hcengineering/workbench'
import { stripTags } from '@hcengineering/text'
import { Person, PersonAccount } from '@hcengineering/contact'
import activity, { ActivityMessage } from '@hcengineering/activity'
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
import {
getCreateBacklinksTxes,
getRemoveBacklinksTxes,
getUpdateBacklinksTxes,
guessBacklinkTx,
isMarkupType,
isCollaborativeType
} from './backlinks'
import { IsChannelMessage, IsDirectMessage, IsMeMentioned, IsThreadMessage } from './utils'
import { IsChannelMessage, IsDirectMessage, IsThreadMessage } from './utils'
/**
* @public
@@ -101,9 +91,10 @@ export async function CommentRemove (
}
const chatMessage = doc as ChatMessage
return await findAll(chunter.class.Backlink, {
backlinkId: chatMessage.attachedTo,
backlinkClass: chatMessage.attachedToClass,
return await findAll(activity.class.ActivityReference, {
srcDocId: chatMessage.attachedTo,
srcDocClass: chatMessage.attachedToClass,
attachedDocId: chatMessage._id
})
}
@@ -254,99 +245,6 @@ async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise
return [updateTx]
}
async function BacklinksCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
if (ctx._class !== core.class.TxCreateDoc) return []
if (control.hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return []
control.storageFx(async (adapter) => {
const txFactory = new TxFactory(control.txFactory.account)
const doc = TxProcessor.createDoc2Doc(ctx)
const targetTx = guessBacklinkTx(control.hierarchy, tx as TxCUD<Doc>)
const txes: Tx[] = await getCreateBacklinksTxes(
control,
adapter,
txFactory,
doc,
targetTx.objectId,
targetTx.objectClass
)
if (txes.length !== 0) {
await control.apply(txes, true)
}
})
return []
}
async function BacklinksUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Doc>
let hasUpdates = false
const attributes = control.hierarchy.getAllAttributes(ctx.objectClass)
for (const attr of attributes.values()) {
if (isMarkupType(attr.type._class) || isCollaborativeType(attr.type._class)) {
if (TxProcessor.txHasUpdate(ctx, attr.name)) {
hasUpdates = true
break
}
}
}
if (hasUpdates) {
const rawDoc = (await control.findAll(ctx.objectClass, { _id: ctx.objectId }))[0]
if (rawDoc !== undefined) {
control.storageFx(async (adapter) => {
const txFactory = new TxFactory(control.txFactory.account)
const doc = TxProcessor.updateDoc2Doc(rawDoc, ctx)
const targetTx = guessBacklinkTx(control.hierarchy, tx as TxCUD<Doc>)
const txes: Tx[] = await getUpdateBacklinksTxes(
control,
adapter,
txFactory,
doc,
targetTx.objectId,
targetTx.objectClass
)
if (txes.length !== 0) {
await control.apply(txes, true)
}
})
}
}
return []
}
async function BacklinksRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc<Doc>
let hasMarkdown = false
const attributes = control.hierarchy.getAllAttributes(ctx.objectClass)
for (const attr of attributes.values()) {
if (isMarkupType(attr.type._class)) {
hasMarkdown = true
break
}
}
if (hasMarkdown) {
const txFactory = new TxFactory(control.txFactory.account)
const txes: Tx[] = await getRemoveBacklinksTxes(control, txFactory, ctx.objectId)
if (txes.length !== 0) {
await control.apply(txes, true)
}
}
return []
}
/**
* @public
*/
@@ -429,27 +327,6 @@ export async function OnDirectMessageSent (originTx: Tx, control: TriggerControl
return res
}
/**
* @public
*/
export async function BacklinkTrigger (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
if (control.hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return []
if (ctx._class === core.class.TxCreateDoc) {
result.push(...(await BacklinksCreate(tx, control)))
}
if (ctx._class === core.class.TxUpdateDoc) {
result.push(...(await BacklinksUpdate(tx, control)))
}
if (ctx._class === core.class.TxRemoveDoc) {
result.push(...(await BacklinksRemove(tx, control)))
}
return result
}
const NOTIFICATION_BODY_SIZE = 50
/**
@@ -468,8 +345,8 @@ export async function getChunterNotificationContent (_: Doc, tx: TxCUD<Doc>): Pr
if (ptx.tx.objectClass === chunter.class.ChatMessage) {
const createTx = ptx.tx as TxCreateDoc<ChatMessage>
message = createTx.attributes.message
} else if (ptx.tx.objectClass === chunter.class.Backlink) {
const createTx = ptx.tx as TxCreateDoc<Backlink>
} else if (ptx.tx.objectClass === activity.class.ActivityReference) {
const createTx = ptx.tx as TxCreateDoc<ActivityReference>
message = createTx.attributes.message
}
}
@@ -580,7 +457,6 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: Trigg
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
BacklinkTrigger,
ChunterTrigger,
OnDirectMessageSent,
OnChatMessageRemoved,
@@ -593,7 +469,6 @@ export default async () => ({
ChunterNotificationContentProvider: getChunterNotificationContent,
IsDirectMessage,
IsThreadMessage,
IsMeMentioned,
IsChannelMessage
}
})
+2 -37
View File
@@ -12,12 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Account, Doc, Hierarchy, Ref, Tx, TxCreateDoc, TxCUD, TxProcessor } from '@hcengineering/core'
import { Account, Doc, Ref, Tx, TxCreateDoc } from '@hcengineering/core'
import { NotificationType } from '@hcengineering/notification'
import { TriggerControl } from '@hcengineering/server-core'
import chunter, { Backlink, DirectMessage } from '@hcengineering/chunter'
import contact, { Person } from '@hcengineering/contact'
import core from '@hcengineering/core/src/component'
import chunter, { DirectMessage } from '@hcengineering/chunter'
import activity from '@hcengineering/activity'
/**
@@ -40,31 +38,6 @@ export async function IsChannelMessage (
return hierarchy.isDerived(doc._class, chunter.class.Channel) || hierarchy.hasMixin(doc, activity.mixin.ActivityDoc)
}
/**
* @public
*/
export async function IsMeMentioned (
originTx: Tx,
doc: Doc,
user: Ref<Account>,
type: NotificationType,
control: TriggerControl
): Promise<boolean> {
const tx = TxProcessor.extractTx(originTx) as TxCUD<Backlink>
if (!isBacklink(tx, control.hierarchy)) {
return false
}
const backlink = TxProcessor.createDoc2Doc(tx as TxCreateDoc<Backlink>)
if (!control.hierarchy.isDerived(backlink.attachedToClass, contact.class.Person)) {
return false
}
const acc = (
await control.modelDb.findAll(contact.class.PersonAccount, { person: backlink.attachedTo as Ref<Person> })
)[0]
return acc._id === user
}
/**
* @public
*/
@@ -85,11 +58,3 @@ export async function IsDirectMessage (
const dm = (await control.findAll(chunter.class.DirectMessage, { _id: doc._id as Ref<DirectMessage> }))[0]
return dm !== undefined
}
function isBacklink (tx: TxCUD<Backlink>, hierarchy: Hierarchy): boolean {
if (tx._class !== core.class.TxCreateDoc) {
return false
}
return hierarchy.isDerived(tx.objectClass, chunter.class.Backlink)
}
-2
View File
@@ -28,7 +28,6 @@ export const serverChunterId = 'server-chunter' as Plugin
*/
export default plugin(serverChunterId, {
trigger: {
BacklinkTrigger: '' as Resource<TriggerFunc>,
ChunterTrigger: '' as Resource<TriggerFunc>,
OnDirectMessageSent: '' as Resource<TriggerFunc>,
OnChatMessageRemoved: '' as Resource<TriggerFunc>,
@@ -41,7 +40,6 @@ export default plugin(serverChunterId, {
IsDirectMessage: '' as TypeMatchFunc,
IsChannelMessage: '' as TypeMatchFunc,
IsThreadMessage: '' as TypeMatchFunc,
IsMeMentioned: '' as TypeMatchFunc,
ChunterNotificationContentProvider: '' as Resource<NotificationContentProvider>
}
})
+115 -175
View File
@@ -14,7 +14,7 @@
// limitations under the License.
//
import chunter, { Backlink, ChatMessage } from '@hcengineering/chunter'
import chunter, { ChatMessage } from '@hcengineering/chunter'
import contact, { Employee, formatName, Person, PersonAccount } from '@hcengineering/contact'
import core, {
Account,
@@ -27,7 +27,6 @@ import core, {
Data,
Doc,
DocumentUpdate,
Hierarchy,
MeasureContext,
MixinUpdate,
Ref,
@@ -45,11 +44,12 @@ import core, {
} from '@hcengineering/core'
import notification, {
ActivityInboxNotification,
BaseNotificationType,
ClassCollaborators,
Collaborators,
CommonInboxNotification,
DocNotifyContext,
InboxNotification,
NotificationProvider,
NotificationType
} from '@hcengineering/notification'
import { getMetadata, getResource } from '@hcengineering/platform'
@@ -57,177 +57,103 @@ import type { TriggerControl } from '@hcengineering/server-core'
import serverNotification, {
getEmployee,
getPersonAccount,
getPersonAccountById,
HTMLPresenter,
TextPresenter
getPersonAccountById
} from '@hcengineering/server-notification'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { Content } from './types'
import { Content, NotifyResult } from './types'
import {
getHTMLPresenter,
getNotificationContent,
getTextPresenter,
isAllowed,
isMixinTx,
isShouldNotify,
isShouldNotifyTx,
isUserEmployeeInFieldValue,
isUserInFieldValue,
replaceAll,
updateNotifyContextsSpace
} from './utils'
export async function OnBacklinkCreate (
originTx: TxCollectionCUD<Doc, AttachedDoc>,
control: TriggerControl
): Promise<Tx[]> {
const hierarchy = control.hierarchy
const isTxCorrect = await isBacklinkCreated(originTx, hierarchy, control)
export function getPushCollaboratorTx (
control: TriggerControl,
user: Ref<Account>,
doc: Doc
): TxMixin<Doc, Doc> | undefined {
const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators)
if (!isTxCorrect) {
return []
}
const tx = originTx as TxCollectionCUD<Doc, Backlink>
const receiver = await getPersonAccount(tx.objectId as Ref<Employee>, control)
if (receiver === undefined) {
return []
}
const sender = await getPersonAccountById(tx.modifiedBy, control)
if (sender === undefined) {
return []
}
if (sender === receiver) {
return []
}
const backlink = TxProcessor.createDoc2Doc(tx.tx as TxCreateDoc<Backlink>)
if (!hierarchy.isDerived(backlink.backlinkClass, activity.class.ActivityMessage)) {
return []
}
const message = (
await control.findAll<ActivityMessage>(
backlink.backlinkClass,
{
_id: backlink.backlinkId as Ref<ActivityMessage>
},
{ limit: 1 }
)
)[0]
if (message === undefined) {
return []
}
const res: Tx[] = []
const collabMixin = hierarchy.as(message as Doc, notification.mixin.Collaborators)
if (collabMixin.collaborators === undefined || !collabMixin.collaborators.includes(receiver._id)) {
const collabTx = control.txFactory.createTxMixin(
message._id,
message._class,
message.space,
notification.mixin.Collaborators,
{
$push: {
collaborators: receiver._id
}
if (mixin.collaborators === undefined || !mixin.collaborators.includes(user)) {
return control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, {
$push: {
collaborators: user
}
)
res.push(collabTx)
})
}
return res
return undefined
}
async function isBacklinkNotified (tx: TxCollectionCUD<Doc, Backlink>, control: TriggerControl): Promise<boolean> {
const receiver = await getPersonAccount(tx.objectId as Ref<Employee>, control)
if (receiver === undefined) {
return false
}
const { hierarchy } = control
const backlink = TxProcessor.createDoc2Doc(tx.tx as TxCreateDoc<Backlink>)
if (!hierarchy.isDerived(backlink.backlinkClass, activity.class.ActivityMessage)) {
return false
}
export async function isMessageAlreadyNotified (
_id: Ref<ActivityMessage>,
user: Ref<Account>,
control: TriggerControl
): Promise<boolean> {
const exists = await control.findAll(
notification.class.ActivityInboxNotification,
{ attachedTo: backlink.backlinkId as Ref<ActivityMessage>, user: receiver._id },
{ limit: 1 }
{ attachedTo: _id, user },
{ limit: 1, projection: { _id: 1 } }
)
return exists.length > 0
}
async function isAlreadyNotified (originTx: TxCUD<Doc>, control: TriggerControl): Promise<boolean> {
if (originTx._class !== core.class.TxCollectionCUD) {
return false
}
const hierarchy = control.hierarchy
const isBacklink = await isBacklinkCreated(originTx as TxCollectionCUD<Doc, AttachedDoc>, hierarchy, control)
if (!isBacklink) {
return false
}
return await isBacklinkNotified(originTx as TxCollectionCUD<Doc, Backlink>, control)
}
async function isBacklinkCreated (
ptx: TxCollectionCUD<Doc, AttachedDoc>,
hierarchy: Hierarchy,
control: TriggerControl
): Promise<boolean> {
if (ptx.tx._class !== core.class.TxCreateDoc || !hierarchy.isDerived(ptx.tx.objectClass, chunter.class.Backlink)) {
return false
}
if (ptx.objectClass === contact.class.Person) {
// We need to check if person is employee.
const [person] = await control.findAll(contact.class.Person, { _id: ptx.objectId as Ref<Person> })
return person !== undefined ? hierarchy.hasMixin(person, contact.mixin.Employee) : false
}
return true
}
/**
* @public
*/
export async function isAllowed (
export async function getCommonNotificationTxes (
control: TriggerControl,
receiver: Ref<PersonAccount>,
typeId: Ref<NotificationType>,
providerId: Ref<NotificationProvider>
): Promise<boolean> {
const setting = (
await control.findAll(
notification.class.NotificationSetting,
{
attachedTo: providerId,
type: typeId,
modifiedBy: receiver
},
{ limit: 1 }
doc: Doc,
data: Partial<Data<CommonInboxNotification>>,
receiver: Ref<Account>,
sender: Ref<Account>,
attachedTo: Ref<Doc>,
attachedToClass: Ref<Class<Doc>>,
space: Ref<Space>,
modifiedOn: Timestamp,
notifyResult: NotifyResult
): Promise<Tx[]> {
const res: Tx[] = []
if (notifyResult.allowed) {
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo })
await pushInboxNotifications(
control,
res,
receiver,
attachedTo,
attachedToClass,
space,
notifyContexts,
data,
notification.class.CommonInboxNotification,
modifiedOn
)
)[0]
if (setting !== undefined) {
return setting.enabled
}
const type = (
await control.modelDb.findAll(notification.class.NotificationType, {
_id: typeId
})
)[0]
if (type === undefined) return false
return type.providers[providerId] ?? false
if (notifyResult.emails.length === 0) {
return res
}
const receiverAccount = await getPersonAccountById(receiver, control)
if (receiverAccount === undefined) {
return res
}
const emp = await getEmployee(receiverAccount.person as Ref<Employee>, control)
if (emp?.active === true) {
for (const type of notifyResult.emails) {
await notifyByEmail(control, type._id, doc, sender as Ref<PersonAccount>, receiverAccount._id)
}
}
return res
}
async function getTextPart (doc: Doc, control: TriggerControl): Promise<string | undefined> {
@@ -243,20 +169,6 @@ async function getHtmlPart (doc: Doc, control: TriggerControl): Promise<string |
return HTMLPresenter != null ? await (await getResource(HTMLPresenter.presenter))(doc, control) : undefined
}
/**
* @public
*/
export function getHTMLPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): HTMLPresenter | undefined {
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.HTMLPresenter)
}
/**
* @public
*/
export function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): TextPresenter | undefined {
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.TextPresenter)
}
function fillTemplate (template: string, sender: string, doc: string, data: string): string {
let res = replaceAll(template, '{sender}', sender)
res = replaceAll(res, '{doc}', doc)
@@ -270,7 +182,7 @@ function fillTemplate (template: string, sender: string, doc: string, data: stri
export async function getContent (
doc: Doc | undefined,
sender: string,
type: Ref<NotificationType>,
type: Ref<BaseNotificationType>,
control: TriggerControl,
data: string
): Promise<Content | undefined> {
@@ -293,7 +205,7 @@ export async function getContent (
async function notifyByEmail (
control: TriggerControl,
type: Ref<NotificationType>,
type: Ref<BaseNotificationType>,
doc: Doc | undefined,
senderId: Ref<PersonAccount>,
receiverId: Ref<PersonAccount>,
@@ -553,7 +465,7 @@ export async function getNotificationTxes (
shouldUpdateTimestamp = true
): Promise<Tx[]> {
const res: Tx[] = []
const notifyResult = await isShouldNotify(control, tx, originTx, object, target, isOwn, isSpace)
const notifyResult = await isShouldNotifyTx(control, tx, originTx, object, target, isOwn, isSpace)
if (notifyResult.allowed) {
await pushActivityInboxNotifications(
@@ -834,11 +746,37 @@ async function removeCollaboratorDoc (tx: TxRemoveDoc<Doc>, control: TriggerCont
}
const res: Tx[] = []
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: tx.objectId })
const notifyContexts = await control.findAll(
notification.class.DocNotifyContext,
{ attachedTo: tx.objectId },
{
projection: {
_id: 1,
_class: 1,
space: 1
}
}
)
if (notifyContexts.length === 0) {
return []
}
const notifyContextRefs = notifyContexts.map(({ _id }) => _id)
const inboxNotifications = await control.findAll(notification.class.InboxNotification, {
docNotifyContext: { $in: notifyContextRefs }
})
const inboxNotifications = await control.findAll(
notification.class.InboxNotification,
{
docNotifyContext: { $in: notifyContextRefs }
},
{
projection: {
_id: 1,
_class: 1,
space: 1
}
}
)
inboxNotifications.forEach((notification) => {
res.push(control.txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id))
@@ -1010,10 +948,6 @@ export async function createCollaboratorNotifications (
return []
}
if (await isAlreadyNotified(originTx ?? tx, control)) {
return []
}
switch (tx._class) {
case core.class.TxCreateDoc:
return await createCollaboratorDoc(tx as TxCreateDoc<Doc>, control, activityMessages, originTx ?? tx)
@@ -1025,8 +959,6 @@ export async function createCollaboratorNotifications (
)
return res
}
case core.class.TxRemoveDoc:
return await removeCollaboratorDoc(tx as TxRemoveDoc<Doc>, control)
case core.class.TxCollectionCUD:
return await collectionCollabDoc(tx as TxCollectionCUD<Doc, AttachedDoc>, control, activityMessages)
}
@@ -1118,6 +1050,14 @@ export async function getCollaborators (
}
}
async function OnDocRemove (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const etx = TxProcessor.extractTx(tx)
if (etx._class !== core.class.TxRemoveDoc) return []
return await removeCollaboratorDoc(tx as TxRemoveDoc<Doc>, control)
}
export * from './types'
export * from './utils'
@@ -1127,8 +1067,8 @@ export default async () => ({
OnChatMessageCreate,
OnAttributeCreate,
OnAttributeUpdate,
OnBacklinkCreate,
OnActivityNotificationViewed
OnActivityNotificationViewed,
OnDocRemove
},
function: {
IsUserInFieldValue: isUserInFieldValue,
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { NotificationType } from '@hcengineering/notification'
import { BaseNotificationType } from '@hcengineering/notification'
/**
* @public
@@ -28,5 +28,5 @@ export interface Content {
*/
export interface NotifyResult {
allowed: boolean
emails: NotificationType[]
emails: BaseNotificationType[]
}
@@ -12,7 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import notification, { NotificationContent, NotificationType } from '@hcengineering/notification'
import notification, {
BaseNotificationType,
CommonNotificationType,
NotificationContent,
NotificationProvider,
NotificationType
} from '@hcengineering/notification'
import type { TriggerControl } from '@hcengineering/server-core'
import core, {
Account,
@@ -28,11 +34,16 @@ import core, {
TxMixin,
TxUpdateDoc
} from '@hcengineering/core'
import serverNotification, { getPersonAccountById, NotificationPresenter } from '@hcengineering/server-notification'
import serverNotification, {
getPersonAccountById,
HTMLPresenter,
NotificationPresenter,
TextPresenter
} from '@hcengineering/server-notification'
import { getResource, IntlString } from '@hcengineering/platform'
import contact, { formatName, Person, PersonAccount } from '@hcengineering/contact'
import { getTextPresenter, isAllowed, NotifyResult } from './index'
import { DocUpdateMessage } from '@hcengineering/activity'
import { NotifyResult } from './types'
/**
* @public
@@ -94,7 +105,60 @@ async function findPersonForAccount (control: TriggerControl, personId: Ref<Pers
return undefined
}
export async function isShouldNotify (
export async function shouldNotifyCommon (
control: TriggerControl,
user: Ref<Account>,
typeId: Ref<CommonNotificationType>
): Promise<NotifyResult> {
const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0]
const emailTypes: BaseNotificationType[] = []
let allowed = false
if (type === undefined) {
return { allowed, emails: emailTypes }
}
if (await isAllowed(control, user as Ref<PersonAccount>, type._id, notification.providers.PlatformNotification)) {
allowed = true
}
if (await isAllowed(control, user as Ref<PersonAccount>, type._id, notification.providers.EmailNotification)) {
emailTypes.push(type)
}
return { allowed, emails: emailTypes }
}
export async function isAllowed (
control: TriggerControl,
receiver: Ref<PersonAccount>,
typeId: Ref<BaseNotificationType>,
providerId: Ref<NotificationProvider>
): Promise<boolean> {
const setting = (
await control.findAll(
notification.class.NotificationSetting,
{
attachedTo: providerId,
type: typeId,
modifiedBy: receiver
},
{ limit: 1 }
)
)[0]
if (setting !== undefined) {
return setting.enabled
}
const type = (
await control.modelDb.findAll(notification.class.BaseNotificationType, {
_id: typeId
})
)[0]
if (type === undefined) return false
return type.providers[providerId] ?? false
}
export async function isShouldNotifyTx (
control: TriggerControl,
tx: TxCUD<Doc>,
originTx: TxCUD<Doc>,
@@ -233,6 +297,14 @@ export function isMixinTx (tx: TxCUD<Doc>): tx is TxMixin<Doc, Doc> {
return tx._class === core.class.TxMixin
}
export function getHTMLPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): HTMLPresenter | undefined {
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.HTMLPresenter)
}
export function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): TextPresenter | undefined {
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.TextPresenter)
}
async function getFallbackNotificationFullfillment (
object: Doc,
originTx: TxCUD<Doc>,
+2 -2
View File
@@ -143,12 +143,12 @@ export default plugin(serverNotificationId, {
NotificationPresenter: '' as Ref<Mixin<NotificationPresenter>>
},
trigger: {
OnBacklinkCreate: '' as Resource<TriggerFunc>,
OnAttributeCreate: '' as Resource<TriggerFunc>,
OnAttributeUpdate: '' as Resource<TriggerFunc>,
OnReactionChanged: '' as Resource<TriggerFunc>,
OnChatMessageCreate: '' as Resource<TriggerFunc>,
OnActivityNotificationViewed: '' as Resource<TriggerFunc>
OnActivityNotificationViewed: '' as Resource<TriggerFunc>,
OnDocRemove: '' as Resource<TriggerFunc>
},
function: {
IsUserInFieldValue: '' as TypeMatchFunc,
+20 -21
View File
@@ -34,9 +34,9 @@ import notification, { CommonInboxNotification } from '@hcengineering/notificati
import { getResource } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import {
getCommonNotificationTxes,
getNotificationContent,
isShouldNotify,
pushInboxNotifications
isShouldNotifyTx
} from '@hcengineering/server-notification-resources'
import task from '@hcengineering/task'
import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker'
@@ -182,32 +182,31 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
return []
}
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: todo._id })
const res: Tx[] = []
const notifyResult = await isShouldNotifyTx(control, createTx, tx, todo, account._id, true, false)
const content = await getNotificationContent(tx, account._id, todo, control)
const details = todo.description != null && todo.description.length > 0 ? todo.description : todo.title
const data: Partial<Data<CommonInboxNotification>> = {
...content,
header: time.string.CreatedToDo,
message: time.string.NewToDoDetails,
props: { details }
}
const notifyResult = await isShouldNotify(control, createTx, tx, todo, account._id, true, false)
if (notifyResult.allowed) {
const content = await getNotificationContent(tx, account._id, todo, control)
const details = todo.description != null && todo.description.length > 0 ? todo.description : todo.title
const data: Partial<Data<CommonInboxNotification>> = {
...content,
header: time.string.CreatedToDo,
message: time.string.NewToDoDetails,
props: { details }
}
await pushInboxNotifications(
res.push(
...(await getCommonNotificationTxes(
control,
res,
todo,
data,
account._id,
tx.modifiedBy,
todo._id,
todo._class,
todo.space,
notifyContexts,
data,
notification.class.CommonInboxNotification,
createTx.modifiedOn
)
}
createTx.modifiedOn,
notifyResult
))
)
return res
}
@@ -4,8 +4,6 @@ import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { IssuesPage } from '../model/tracker/issues-page'
import { IssuesDetailsPage } from '../model/tracker/issues-details-page'
import { NewIssue } from '../model/tracker/types'
import { ContactsNavigationMenuPage } from '../model/contacts/navigation-menu-page'
import { EmployeesPage } from '../model/contacts/employees-page'
import { EmployeeDetailsPage } from '../model/contacts/employee-details-page'
test.use({
@@ -68,38 +66,6 @@ test.describe('Mentions issue tests', () => {
await issuesDetailsPage.checkCollaborators(['Appleseed John', 'Dirak Kainin'])
})
test('Check that the backlink shown in the Contact activity', async ({ page }) => {
const mentionName = 'Dirak Kainin'
const mentionIssue: NewIssue = {
title: `Check that the backlink shown in the Contact activity-${generateId()}`,
description: 'Check that the backlink shown in the Contact activity description'
}
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonTracker.click()
const issuesPage = new IssuesPage(page)
await issuesPage.modelSelectorAll.click()
await issuesPage.createNewIssue(mentionIssue)
await issuesPage.searchIssueByName(mentionIssue.title)
await issuesPage.openIssueByName(mentionIssue.title)
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.addMentions(mentionName)
await issuesDetailsPage.checkCommentExist(`@${mentionName}`)
await leftSideMenuPage.buttonContacts.click()
const contactsNavigationMenuPage = new ContactsNavigationMenuPage(page)
await contactsNavigationMenuPage.buttonEmployee.click()
const employeesPage = new EmployeesPage(page)
await employeesPage.openEmployeeByName(mentionName)
const employeeDetailsPage = new EmployeeDetailsPage(page)
await employeeDetailsPage.checkActivityExist(`mentioned ${mentionName} in`, `@${mentionName}`)
})
test('Check that the backlink shown in the Issue activity', async ({ page }) => {
const mentionName = 'Dirak Kainin'
const backlinkIssue: NewIssue = {