diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index b1d5a6132b..7312194d72 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -362,6 +362,7 @@ specifiers: rfc6902: ^5.0.1 sass: ^1.53.0 sass-loader: ^13.2.0 + saxes: ^6.0.0 sharp: ~0.30.7 simplytyped: ^3.3.0 smartcrop: ~2.0.5 @@ -753,6 +754,7 @@ dependencies: rfc6902: 5.0.1 sass: 1.56.1 sass-loader: 13.2.0_sass@1.56.1+webpack@5.75.0 + saxes: 6.0.0 sharp: 0.30.7 simplytyped: 3.3.0_typescript@4.8.4 smartcrop: 2.0.5 @@ -15128,6 +15130,13 @@ packages: xmlchars: 2.2.0 dev: false + /saxes/6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + /scheduler/0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -21263,7 +21272,7 @@ packages: dev: false file:projects/server-tracker-resources.tgz: - resolution: {integrity: sha512-t7nGyhfhEjSqEWWptyy3dts4WixZayRj/3aGr66LWM1JXQaGfBgGOTjBD1FKFJptRcAJngE3+39D6vqPOqx0VQ==, tarball: file:projects/server-tracker-resources.tgz} + resolution: {integrity: sha512-T7YWkcgBG3DOJu188v9G42cTL0oSlOViSqtPuG9BPbwpFiojMA859qmciaZK1tdYvqgq+4XnUgHmRCslNSEKYg==, tarball: file:projects/server-tracker-resources.tgz} name: '@rush-temp/server-tracker-resources' version: 0.0.0 dependencies: @@ -22018,7 +22027,7 @@ packages: dev: false file:projects/text.tgz_a7abb371f17d2bc77abec7bc53398db5: - resolution: {integrity: sha512-QfLKyYxcLXLoqBMnbP4k2ND5871b+ycKCz7+NZ7xBtPtEb3Ee6jPdFrf8wbmnIYX/dLV0VThNKXcJ4ZiWxNvbA==, tarball: file:projects/text.tgz} + resolution: {integrity: sha512-G5lQZT9m9iBfZJlr+/pwyq5NCU5bEG+JMbxjf2npi9wSxa0/1EwWMAAk4WJ+AAKBq51GI+uAHVhXYhDEQzMHDA==, tarball: file:projects/text.tgz} id: file:projects/text.tgz name: '@rush-temp/text' version: 0.0.0 @@ -22051,6 +22060,7 @@ packages: eslint-plugin-n: 15.5.1_eslint@8.27.0 eslint-plugin-promise: 6.1.1_eslint@8.27.0 prettier: 2.8.8 + saxes: 6.0.0 typescript: 4.8.4 transitivePeerDependencies: - prosemirror-keymap diff --git a/models/server-chunter/src/index.ts b/models/server-chunter/src/index.ts index 27390bf7d7..a7be491654 100644 --- a/models/server-chunter/src/index.ts +++ b/models/server-chunter/src/index.ts @@ -41,6 +41,10 @@ export function createModel (builder: Builder): void { } ) + builder.mixin(chunter.class.DirectMessage, core.class.Class, serverNotification.mixin.NotificationPresenter, { + presenter: serverChunter.function.ChunterNotificationContentProvider + }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverChunter.trigger.BacklinkTrigger }) diff --git a/models/server-notification/src/index.ts b/models/server-notification/src/index.ts index 86ab76bac6..6b4418bea2 100644 --- a/models/server-notification/src/index.ts +++ b/models/server-notification/src/index.ts @@ -24,9 +24,11 @@ import { Resource } from '@hcengineering/platform' import serverCore, { TriggerControl } from '@hcengineering/server-core' import serverNotification, { HTMLPresenter, + NotificationPresenter, Presenter, TextPresenter, - TypeMatch + TypeMatch, + NotificationContentProvider } from '@hcengineering/server-notification' export { serverNotificationId } from '@hcengineering/server-notification' @@ -41,6 +43,11 @@ export class TTextPresenter extends TClass implements TextPresenter { presenter!: Resource } +@Mixin(serverNotification.mixin.NotificationPresenter, core.class.Class) +export class TNotificationPresenter extends TClass implements NotificationPresenter { + presenter!: Resource +} + @Mixin(serverNotification.mixin.TypeMatch, notification.class.NotificationType) export class TTypeMatch extends TNotificationType implements TypeMatch { func!: Resource< @@ -49,7 +56,7 @@ export class TTypeMatch extends TNotificationType implements TypeMatch { } export function createModel (builder: Builder): void { - builder.createModel(THTMLPresenter, TTextPresenter, TTypeMatch) + builder.createModel(THTMLPresenter, TTextPresenter, TTypeMatch, TNotificationPresenter) builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverNotification.trigger.OnBacklinkCreate diff --git a/models/server-tracker/src/index.ts b/models/server-tracker/src/index.ts index 559f416be0..c4c80b72bc 100644 --- a/models/server-tracker/src/index.ts +++ b/models/server-tracker/src/index.ts @@ -32,6 +32,10 @@ export function createModel (builder: Builder): void { presenter: serverTracker.function.IssueTextPresenter }) + builder.mixin(tracker.class.Issue, core.class.Class, serverNotification.mixin.NotificationPresenter, { + presenter: serverTracker.function.IssueNotificationContentProvider + }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverTracker.trigger.OnIssueUpdate }) diff --git a/packages/text/src/html.ts b/packages/text/src/html.ts index da963510d3..e664d207eb 100644 --- a/packages/text/src/html.ts +++ b/packages/text/src/html.ts @@ -17,6 +17,7 @@ import { Extensions, getSchema } from '@tiptap/core' import { generateJSON, generateHTML } from '@tiptap/html' import { Node as ProseMirrorNode } from '@tiptap/pm/model' +import { defaultExtensions } from './extensions' /** * @public */ @@ -33,3 +34,48 @@ export function parseHTML (content: string, extensions: Extensions): ProseMirror return ProseMirrorNode.fromJSON(schema, json) } + +const ELLIPSIS_CHAR = '…' +const WHITESPACE = ' ' + +/** + * @public + */ +export function stripTags (htmlString: string, textLimit = 0, extensions: Extensions | undefined = undefined): string { + const effectiveExtensions = extensions ?? defaultExtensions + const parsed = parseHTML(htmlString, effectiveExtensions) + + const textParts: string[] = [] + let charCount = 0 + let isHardStop = false + + parsed.descendants((node, _pos, parent): boolean => { + if (isHardStop) { + return false + } + + if (node.type.isText) { + const text = node.text ?? '' + if (textLimit > 0 && charCount + text.length > textLimit) { + const toAddCount = textLimit - charCount + const textPart = text.substring(0, toAddCount) + textParts.push(textPart) + textParts.push(ELLIPSIS_CHAR) + isHardStop = true + } else { + textParts.push(text) + charCount += text.length + } + return false + } else if (node.type.isBlock) { + if (textParts.length > 0 && textParts[textParts.length - 1] !== WHITESPACE) { + textParts.push(WHITESPACE) + charCount++ + } + } + return true + }) + + const result = textParts.join('') + return result +} diff --git a/plugins/chunter-assets/lang/en.json b/plugins/chunter-assets/lang/en.json index 477eef243e..a0a019e44c 100644 --- a/plugins/chunter-assets/lang/en.json +++ b/plugins/chunter-assets/lang/en.json @@ -76,6 +76,8 @@ "LastMessage": "Last message", "You": "You", "YouHaveJoinedTheConversation": "You have joined the conversation", - "NoMessages": "There are no messages yet" + "NoMessages": "There are no messages yet", + "DirectNotificationTitle": "{senderName}", + "DirectNotificationBody": "{message}" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/ru.json b/plugins/chunter-assets/lang/ru.json index 53c032ef16..0bc1fdb1cb 100644 --- a/plugins/chunter-assets/lang/ru.json +++ b/plugins/chunter-assets/lang/ru.json @@ -76,6 +76,8 @@ "LastMessage": "Последнее сообщение", "You": "Вы", "YouHaveJoinedTheConversation": "Вы присоединились к диалогу", - "NoMessages": "Сообщений пока нет" + "NoMessages": "Сообщений пока нет", + "DirectNotificationTitle": "{senderName}", + "DirectNotificationBody": "{message}" } } \ No newline at end of file diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts index 9a7707723c..6432b37954 100644 --- a/plugins/chunter/src/index.ts +++ b/plugins/chunter/src/index.ts @@ -182,7 +182,9 @@ export default plugin(chunterId, { Message: '' as IntlString, MessageOn: '' as IntlString, UnarchiveConfirm: '' as IntlString, - ConvertToPrivate: '' as IntlString + ConvertToPrivate: '' as IntlString, + DirectNotificationTitle: '' as IntlString, + DirectNotificationBody: '' as IntlString }, resolver: { Location: '' as Resource<(loc: Location) => Promise> diff --git a/plugins/notification-assets/lang/en.json b/plugins/notification-assets/lang/en.json index aa666c761b..6525ed4fb8 100644 --- a/plugins/notification-assets/lang/en.json +++ b/plugins/notification-assets/lang/en.json @@ -25,6 +25,8 @@ "People": "People", "All": "All", "Read": "Read", - "Unread": "Unread" + "Unread": "Unread", + "CommonNotificationTitle": "{title}", + "CommonNotificationBody": "Updated by {senderName}" } } diff --git a/plugins/notification-assets/lang/ru.json b/plugins/notification-assets/lang/ru.json index 6b9abd9f32..acde37b24a 100644 --- a/plugins/notification-assets/lang/ru.json +++ b/plugins/notification-assets/lang/ru.json @@ -25,6 +25,8 @@ "People": "Люди", "All": "Все", "Read": "Прочитанное", - "Unread": "Не прочитанное" + "Unread": "Не прочитанное", + "CommonNotificationTitle": "{title}", + "CommonNotificationBody": "Обновление от {senderName}" } } diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index b51e9832fa..b9b37152a1 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -95,6 +95,16 @@ export interface NotificationTemplate { subjectTemplate: string } +/** + * @public + */ +export interface NotificationContent { + title: IntlString + body: IntlString + intlParams: Record + intlParamsNotLocalized?: Record +} + /** * @public */ @@ -171,6 +181,10 @@ export interface DocUpdateTx { modifiedBy: Ref modifiedOn: Timestamp isNew: boolean + title?: IntlString + body?: IntlString + intlParams?: Record + intlParamsNotLocalized?: Record } /** @@ -263,7 +277,9 @@ const notification = plugin(notificationId, { Notification: '' as IntlString, Notifications: '' as IntlString, DontTrack: '' as IntlString, - Inbox: '' as IntlString + Inbox: '' as IntlString, + CommonNotificationTitle: '' as IntlString, + CommonNotificationBody: '' as IntlString }, function: { GetNotificationClient: '' as Resource diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index 8fa770182e..8a2607d429 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -274,7 +274,13 @@ "UnsetParent": "Parent issue will be unset", "Unarchive": "Unarchive", "UnarchiveConfirm": "Do you want to unarchive project?", - "AllProjects": "All projects" + "AllProjects": "All projects", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "Updated by {senderName}", + "IssueNotificationChanged": "{senderName} changed {property}", + "IssueNotificationChangedProperty": "{senderName} changed {property} to \"{newValue}\"", + "IssueNotificationMessage": "{senderName}: {message}", + "IssueAssigneedToYou": "Assigned to you" }, "status": {} } diff --git a/plugins/tracker-assets/lang/ru.json b/plugins/tracker-assets/lang/ru.json index 3b3593ed59..a78ad5240a 100644 --- a/plugins/tracker-assets/lang/ru.json +++ b/plugins/tracker-assets/lang/ru.json @@ -274,7 +274,13 @@ "UnsetParent": "Родительская задача будет убрана", "Unarchive": "Разархивировать", "UnarchiveConfirm": "Вы действительно хотите разархивировать?", - "AllProjects": "All projects" + "AllProjects": "All projects", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "Обновлено {senderName}", + "IssueNotificationChanged": "{senderName} изменил {property}", + "IssueNotificationChangedProperty": "{senderName} изменил {property} на \"{newValue}\"", + "IssueNotificationMessage": "{senderName}: {message}", + "IssueAssigneedToYou": "Назначено вам" }, "status": {} } diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index 64e8caea55..0d227659d2 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -481,7 +481,13 @@ export default plugin(trackerId, { }, string: { ConfigLabel: '' as IntlString, - NewRelatedIssue: '' as IntlString + NewRelatedIssue: '' as IntlString, + IssueNotificationTitle: '' as IntlString, + IssueNotificationBody: '' as IntlString, + IssueNotificationChanged: '' as IntlString, + IssueNotificationChangedProperty: '' as IntlString, + IssueNotificationMessage: '' as IntlString, + IssueAssigneedToYou: '' as IntlString }, mixin: { ProjectIssueTargetOptions: '' as Ref> diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 88d7fb10ec..fb327166d3 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -43,11 +43,12 @@ import core, { TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core' -import notification, { Collaborators, NotificationType } from '@hcengineering/notification' -import { getMetadata } from '@hcengineering/platform' +import notification, { Collaborators, NotificationType, NotificationContent } from '@hcengineering/notification' +import { getMetadata, IntlString } from '@hcengineering/platform' import serverCore, { TriggerControl } from '@hcengineering/server-core' import { getDocCollaborators, getMixinTx, pushNotification } from '@hcengineering/server-notification-resources' import { workbenchId } from '@hcengineering/workbench' +import { stripTags } from '@hcengineering/text' import { getBacklinks } from './backlinks' function getCreateBacklinksTxes ( @@ -442,7 +443,7 @@ export async function OnMessageSent (tx: Tx, control: TriggerControl): Promise, + target: Ref, + control: TriggerControl +): Promise { + const title: IntlString = chunter.string.DirectNotificationTitle + let body: IntlString = chunter.string.Message + const intlParams: Record = {} + + if (tx._class === core.class.TxCollectionCUD) { + const ptx = tx as TxCollectionCUD + if (ptx.tx._class === core.class.TxCreateDoc) { + if (ptx.tx.objectClass === chunter.class.Message) { + const createTx = ptx.tx as TxCreateDoc + const message = createTx.attributes.content + const plainTextMessage = stripTags(message, NOTIFICATION_BODY_SIZE) + intlParams.message = plainTextMessage + body = chunter.string.DirectNotificationBody + } + } + } + + return { + title, + body, + intlParams + } +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { @@ -547,6 +582,7 @@ export default async () => ({ CommentRemove, ChannelHTMLPresenter: channelHTMLPresenter, ChannelTextPresenter: channelTextPresenter, + ChunterNotificationContentProvider: getChunterNotificationContent, IsDirectMessage, IsThreadMessage, IsMeMentioned, diff --git a/server-plugins/chunter/src/index.ts b/server-plugins/chunter/src/index.ts index 88c64b52bc..ba3854873a 100644 --- a/server-plugins/chunter/src/index.ts +++ b/server-plugins/chunter/src/index.ts @@ -17,7 +17,7 @@ import { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref } fr import type { Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { TriggerFunc } from '@hcengineering/server-core' -import { Presenter, TypeMatchFunc } from '@hcengineering/server-notification' +import { Presenter, TypeMatchFunc, NotificationContentProvider } from '@hcengineering/server-notification' /** * @public @@ -50,6 +50,7 @@ export default plugin(serverChunterId, { IsDirectMessage: '' as TypeMatchFunc, IsChannelMessage: '' as TypeMatchFunc, IsThreadMessage: '' as TypeMatchFunc, - IsMeMentioned: '' as TypeMatchFunc + IsMeMentioned: '' as TypeMatchFunc, + ChunterNotificationContentProvider: '' as Resource } }) diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index f0dfd301ef..ae48f26f7e 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -45,16 +45,18 @@ import notification, { ClassCollaborators, Collaborators, DocUpdates, + DocUpdateTx, EmailNotification, NotificationProvider, NotificationType } from '@hcengineering/notification' -import { getResource } from '@hcengineering/platform' +import { IntlString, getResource } from '@hcengineering/platform' import type { TriggerControl } from '@hcengineering/server-core' import serverNotification, { HTMLPresenter, TextPresenter, getEmployee, + NotificationPresenter, getPersonAccount, getPersonAccountById } from '@hcengineering/server-notification' @@ -182,6 +184,10 @@ export function getTextPresenter (_class: Ref>, hierarchy: Hierarchy) return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.TextPresenter) } +function getNotificationPresenter (_class: Ref>, hierarchy: Hierarchy): NotificationPresenter | undefined { + return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.NotificationPresenter) +} + function fillTemplate (template: string, sender: string, doc: string, data: string): string { let res = replaceAll(template, '{sender}', sender) res = replaceAll(res, '{doc}', doc) @@ -462,10 +468,43 @@ async function isShouldNotify ( } } +async function findPersonForAccount (control: TriggerControl, personId: Ref): Promise { + const persons = await control.findAll(contact.class.Person, { _id: personId }) + if (persons !== undefined && persons.length > 0) { + return persons[0] + } + return undefined +} + +async function getFallbackNotificationFullfillment ( + object: Doc, + originTx: TxCUD, + control: TriggerControl +): Promise> { + const intlParams: Record = {} + + const textPresenter = getTextPresenter(object._class, control.hierarchy) + if (textPresenter !== undefined) { + const textPresenterFunc = await getResource(textPresenter.presenter) + intlParams.title = await textPresenterFunc(object, control) + } + + const account = control.modelDb.getObject(originTx.modifiedBy) as PersonAccount + if (account !== undefined) { + const senderPerson = await findPersonForAccount(control, account.person) + if (senderPerson !== undefined) { + const senderName = formatName(senderPerson.name) + intlParams.senderName = senderName + } + } + + return intlParams +} + /** * @public */ -export function pushNotification ( +export async function pushNotification ( control: TriggerControl, res: Tx[], target: Ref, @@ -473,7 +512,46 @@ export function pushNotification ( originTx: TxCUD, docUpdates: DocUpdates[], modifiedBy?: Ref -): void { +): Promise { + let title: IntlString = notification.string.CommonNotificationTitle + let body: IntlString = notification.string.CommonNotificationBody + let intlParams: Record = await getFallbackNotificationFullfillment(object, originTx, control) + let intlParamsNotLocalized: Record | undefined + + const notificationPresenter = getNotificationPresenter(object._class, control.hierarchy) + if (notificationPresenter !== undefined) { + const getFuillfillmentParams = await getResource(notificationPresenter.presenter) + const updateIntlParams = await getFuillfillmentParams(object, originTx, target, control) + title = updateIntlParams.title + body = updateIntlParams.body + intlParams = { + ...intlParams, + ...updateIntlParams.intlParams + } + if (updateIntlParams.intlParamsNotLocalized != null) { + intlParamsNotLocalized = updateIntlParams.intlParamsNotLocalized + } + } + + const tx: DocUpdateTx = { + _id: originTx._id, + modifiedOn: originTx.modifiedOn, + modifiedBy: modifiedBy ?? originTx.modifiedBy, + isNew: true + } + if (title !== undefined) { + tx.title = title + } + if (body !== undefined) { + tx.body = body + } + if (intlParams !== undefined) { + tx.intlParams = intlParams + } + if (intlParamsNotLocalized !== undefined) { + tx.intlParamsNotLocalized = intlParamsNotLocalized + } + const current = docUpdates.find((p) => p.user === target) if (current === undefined) { res.push( @@ -483,27 +561,13 @@ export function pushNotification ( attachedToClass: object._class, hidden: false, lastTxTime: originTx.modifiedOn, - txes: [ - { - _id: originTx._id, - modifiedOn: originTx.modifiedOn, - modifiedBy: modifiedBy ?? originTx.modifiedBy, - isNew: true - } - ] + txes: [tx] }) ) } else { res.push( control.txFactory.createTxUpdateDoc(current._class, current.space, current._id, { - $push: { - txes: { - _id: originTx._id, - modifiedOn: originTx.modifiedOn, - modifiedBy: modifiedBy ?? originTx.modifiedBy, - isNew: true - } - } + $push: { txes: tx } }) ) res.push( @@ -528,7 +592,7 @@ async function getNotificationTxes ( const res: Tx[] = [] const allowed = await isShouldNotify(control, tx, originTx, object, target, isOwn, isSpace) if (allowed.allowed) { - pushNotification(control, res, target, object, originTx, docUpdates) + await pushNotification(control, res, target, object, originTx, docUpdates) } if (allowed.emails.length === 0) return res const acc = await getPersonAccountById(target, control) @@ -711,7 +775,7 @@ async function updateCollaboratorsMixin ( attachedTo: tx.objectId }) for (const collab of newCollabs) { - pushNotification(control, res, collab, prevDoc, originTx, docUpdates) + await pushNotification(control, res, collab, prevDoc, originTx, docUpdates) } } } diff --git a/server-plugins/notification/src/index.ts b/server-plugins/notification/src/index.ts index 221d397544..93e94d92ca 100644 --- a/server-plugins/notification/src/index.ts +++ b/server-plugins/notification/src/index.ts @@ -15,8 +15,8 @@ // import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' -import { Account, Class, Doc, Mixin, Ref, Tx } from '@hcengineering/core' -import { NotificationType } from '@hcengineering/notification' +import { Account, Class, Doc, Mixin, Ref, Tx, TxCUD } from '@hcengineering/core' +import { NotificationType, NotificationContent } from '@hcengineering/notification' import { Plugin, Resource, plugin } from '@hcengineering/platform' import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core' @@ -112,6 +112,23 @@ export interface TypeMatch extends NotificationType { func: TypeMatchFunc } +/** + * @public + */ +export type NotificationContentProvider = ( + doc: Doc, + tx: TxCUD, + target: Ref, + control: TriggerControl +) => Promise + +/** + * @public + */ +export interface NotificationPresenter extends Class { + presenter: Resource +} + /** * @public */ @@ -119,7 +136,8 @@ export default plugin(serverNotificationId, { mixin: { HTMLPresenter: '' as Ref>, TextPresenter: '' as Ref>, - TypeMatch: '' as Ref> + TypeMatch: '' as Ref>, + NotificationPresenter: '' as Ref> }, trigger: { OnBacklinkCreate: '' as Resource, diff --git a/server-plugins/tracker-resources/package.json b/server-plugins/tracker-resources/package.json index e4fc720381..60b7c5ce67 100644 --- a/server-plugins/tracker-resources/package.json +++ b/server-plugins/tracker-resources/package.json @@ -31,11 +31,13 @@ "@hcengineering/server-core": "^0.6.1", "@hcengineering/tracker": "^0.6.11", "@hcengineering/contact": "^0.6.19", + "@hcengineering/chunter": "^0.6.10", "@hcengineering/notification": "^0.6.14", "@hcengineering/task": "^0.6.11", "@hcengineering/view": "^0.6.8", "@hcengineering/login": "^0.6.7", "@hcengineering/workbench": "^0.6.8", - "@hcengineering/server-task-resources": "^0.6.0" + "@hcengineering/server-task-resources": "^0.6.0", + "@hcengineering/text": "^0.6.0" } } diff --git a/server-plugins/tracker-resources/src/index.ts b/server-plugins/tracker-resources/src/index.ts index c543068014..17ae30c0e0 100644 --- a/server-plugins/tracker-resources/src/index.ts +++ b/server-plugins/tracker-resources/src/index.ts @@ -14,6 +14,7 @@ // import core, { + Account, AttachedDoc, concatLink, Doc, @@ -29,11 +30,16 @@ import core, { TxUpdateDoc, WithLookup } from '@hcengineering/core' -import { getMetadata } from '@hcengineering/platform' +import { getMetadata, IntlString } from '@hcengineering/platform' +import { Person, PersonAccount } from '@hcengineering/contact' import serverCore, { TriggerControl } from '@hcengineering/server-core' import tracker, { Component, Issue, IssueParentInfo, TimeSpendReport, trackerId } from '@hcengineering/tracker' +import { NotificationContent } from '@hcengineering/notification' import { workbenchId } from '@hcengineering/workbench' +import chunter, { Comment } from '@hcengineering/chunter' +import { stripTags } from '@hcengineering/text' + async function updateSubIssues ( updateTx: TxUpdateDoc, control: TriggerControl, @@ -69,6 +75,83 @@ export async function issueTextPresenter (doc: Doc, control: TriggerControl): Pr return issueName } +function isSamePerson (control: TriggerControl, assignee: Ref, target: Ref): boolean { + const targetAccount = control.modelDb.getObject(target) as PersonAccount + return assignee === targetAccount?.person +} + +const NOTIFICATION_BODY_SIZE = 50 +/** + * @public + */ +export async function getIssueNotificationContent ( + doc: Doc, + tx: TxCUD, + target: Ref, + control: TriggerControl +): Promise { + const issue = doc as Issue + + const issueShortName = await issueTextPresenter(doc, control) + const issueTitle = `${issueShortName}: ${issue.title}` + + const title = tracker.string.IssueNotificationTitle + let body = tracker.string.IssueNotificationBody + const intlParams: Record = { + issueTitle + } + const intlParamsNotLocalized: Record = {} + + if (tx._class === core.class.TxCollectionCUD) { + const ptx = tx as TxCollectionCUD + + if (ptx.tx._class === core.class.TxCreateDoc) { + if (ptx.tx.objectClass === chunter.class.Comment) { + const createTx = ptx.tx as TxCreateDoc + const message = createTx.attributes.message + const plainTextMessage = stripTags(message, NOTIFICATION_BODY_SIZE) + intlParams.message = plainTextMessage + } + } else if (ptx.tx._class === core.class.TxUpdateDoc) { + const updateTx = ptx.tx as TxUpdateDoc + + if ( + updateTx.operations.assignee !== null && + updateTx.operations.assignee !== undefined && + isSamePerson(control, updateTx.operations.assignee, target) + ) { + body = tracker.string.IssueAssigneedToYou + } else { + const attributes = control.hierarchy.getAllAttributes(doc._class) + for (const attrName in updateTx.operations) { + if (!Object.prototype.hasOwnProperty.call(updateTx.operations, attrName)) { + continue + } + + const attr = attributes.get(attrName) + if (attr !== null && attr !== undefined) { + intlParamsNotLocalized.property = attr.label + if (attr.type._class === core.class.TypeString) { + body = tracker.string.IssueNotificationChangedProperty + intlParams.newValue = (issue as any)[attr.name]?.toString() + } else { + body = tracker.string.IssueNotificationChanged + } + } + break + } + } + } + } + + return { + title, + body, + intlParams, + intlParamsNotLocalized + } +} + /** * @public */ @@ -157,7 +240,8 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise ({ function: { IssueHTMLPresenter: issueHTMLPresenter, - IssueTextPresenter: issueTextPresenter + IssueTextPresenter: issueTextPresenter, + IssueNotificationContentProvider: getIssueNotificationContent }, trigger: { OnIssueUpdate, diff --git a/server-plugins/tracker/src/index.ts b/server-plugins/tracker/src/index.ts index 11cbd7dacc..097e257c4b 100644 --- a/server-plugins/tracker/src/index.ts +++ b/server-plugins/tracker/src/index.ts @@ -16,7 +16,7 @@ import type { Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { TriggerFunc } from '@hcengineering/server-core' -import { Presenter } from '@hcengineering/server-notification' +import { Presenter, NotificationContentProvider } from '@hcengineering/server-notification' /** * @public @@ -29,7 +29,8 @@ export const serverTrackerId = 'server-tracker' as Plugin export default plugin(serverTrackerId, { function: { IssueHTMLPresenter: '' as Resource, - IssueTextPresenter: '' as Resource + IssueTextPresenter: '' as Resource, + IssueNotificationContentProvider: '' as Resource }, trigger: { OnIssueUpdate: '' as Resource,