Improve notifications (#5378)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-04-17 01:20:26 +05:00 committed by GitHub
parent de6e0739c2
commit 11599a51bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 162 additions and 40 deletions

View File

@ -42,10 +42,6 @@ export function createModel (builder: Builder): void {
}
)
builder.mixin(chunter.class.DirectMessage, core.class.Class, serverNotification.mixin.NotificationPresenter, {
presenter: serverChunter.function.ChunterNotificationContentProvider
})
builder.mixin(chunter.class.ChatMessage, core.class.Class, serverNotification.mixin.NotificationPresenter, {
presenter: serverChunter.function.ChunterNotificationContentProvider
})

View File

@ -38,7 +38,7 @@
"Mentioned": "Mentioned",
"You": "You",
"Mentions": "Mentions",
"MentionedYouIn": "Mentioned you in",
"MentionedYouIn": "Mentioned you in {title}",
"Messages": "Messages",
"Thread": "Thread"
}

View File

@ -37,6 +37,6 @@
"Mentioned": "Mencionado",
"You": "Tú",
"Mentions": "Menciones",
"MentionedYouIn": "Has sido Mencionado en"
"MentionedYouIn": "Has sido Mencionado en {title}"
}
}

View File

@ -37,6 +37,6 @@
"Mentioned": "Mencionado",
"You": "Tu",
"Mentions": "Menções",
"MentionedYouIn": "Foste Mencionado em"
"MentionedYouIn": "Foste Mencionado em {title}"
}
}

View File

@ -38,7 +38,7 @@
"Mentioned": "Упомянул(а)",
"You": "Вы",
"Mentions": "Упоминания",
"MentionedYouIn": "Упомянул(а) вас в",
"MentionedYouIn": "Упомянул(а) вас в {title}",
"Messages": "Cообщения",
"Thread": "Обсуждение"
}

View File

@ -76,6 +76,7 @@
"NoMessages": "There are no messages yet",
"DirectNotificationTitle": "{senderName}",
"DirectNotificationBody": "{message}",
"MessageNotificationBody": "{senderName}: {message}",
"AddCommentPlaceholder": "Add comment...",
"Reacted": "Reacted",
"Docs": "Documents",

View File

@ -76,6 +76,7 @@
"NoMessages": "No hay mensajes todavía",
"DirectNotificationTitle": "{senderName}",
"DirectNotificationBody": "{message}",
"MessageNotificationBody": "{senderName}: {message}",
"AddCommentPlaceholder": "Añadir comentario...",
"Reacted": "Reaccionó",
"Docs": "Documentos",

View File

@ -76,6 +76,7 @@
"NoMessages": "Ainda não existem mensagens",
"DirectNotificationTitle": "{senderName}",
"DirectNotificationBody": "{message}",
"MessageNotificationBody": "{senderName}: {message}",
"AddCommentPlaceholder": "Adicionar comentário...",
"Reacted": "Reagiu",
"Docs": "Documentos",

View File

@ -76,6 +76,7 @@
"NoMessages": "Сообщений пока нет",
"DirectNotificationTitle": "{senderName}",
"DirectNotificationBody": "{message}",
"MessageNotificationBody": "{senderName}: {message}",
"AddCommentPlaceholder": "Добавить комментарий...",
"Reacted": "Отреагировал(а)",
"Docs": "Documents",

View File

@ -72,7 +72,6 @@ export default mergeIds(chunterId, chunter, {
In: '' as IntlString,
Replies: '' as IntlString,
Topic: '' as IntlString,
Thread: '' as IntlString,
Threads: '' as IntlString,
New: '' as IntlString,
GetNewReplies: '' as IntlString,

View File

@ -173,10 +173,12 @@ export default plugin(chunterId, {
ConvertToPrivate: '' as IntlString,
DirectNotificationTitle: '' as IntlString,
DirectNotificationBody: '' as IntlString,
MessageNotificationBody: '' as IntlString,
AddCommentPlaceholder: '' as IntlString,
LeftComment: '' as IntlString,
Docs: '' as IntlString,
Chat: '' as IntlString,
Thread: '' as IntlString,
ThreadMessage: '' as IntlString,
ReplyToThread: '' as IntlString,
Channels: '' as IntlString,

View File

@ -29,6 +29,8 @@
"Unread": "Unread",
"CommonNotificationTitle": "{title}",
"CommonNotificationBody": "Updated by {senderName}",
"CommonNotificationChanged": "{senderName} changed {property}",
"CommonNotificationChangedProperty": "{senderName} changed {property} to \"{newValue}\"",
"ChangedCollaborators": "Changed collaborators",
"NewCollaborators": "New collaborators",
"RemovedCollaborators": "Removed collaborators",

View File

@ -28,6 +28,8 @@
"Unread": "No leído",
"CommonNotificationTitle": "{title}",
"CommonNotificationBody": "Actualizado por {senderName}",
"CommonNotificationChanged": "{senderName} cambió {property}",
"CommonNotificationChangedProperty": "{senderName} cambió {property} a \"{newValue}\"",
"ChangedCollaborators": "Colaboradores Cambiados",
"NewCollaborators": "Nuevos colaboradores",
"RemovedCollaborators": "Colaboradores Eliminados",

View File

@ -28,6 +28,8 @@
"Unread": "Não lido",
"CommonNotificationTitle": "{title}",
"CommonNotificationBody": "Atualizado por {senderName}",
"CommonNotificationChanged": "{senderName} alterou {property}",
"CommonNotificationChangedProperty": "{senderName} alterou {property} para \"{newValue}\"",
"ChangedCollaborators": "Colaboradores alterados",
"NewCollaborators": "Novos colaboradores",
"RemovedCollaborators": "Colaboradores removidos",

View File

@ -29,6 +29,8 @@
"Unread": "Не прочитанное",
"CommonNotificationTitle": "{title}",
"CommonNotificationBody": "Обновление от {senderName}",
"CommonNotificationChanged": "{senderName} изменил {property}",
"CommonNotificationChangedProperty": "{senderName} изменил {property} на \"{newValue}\"",
"ChangedCollaborators": "Изменены участники",
"NewCollaborators": "Новые участники",
"RemovedCollaborators": "Удалены участники",

View File

@ -16,7 +16,8 @@
import notification, {
ActivityInboxNotification,
DocNotifyContext,
InboxNotification
InboxNotification,
decodeObjectURI
} from '@hcengineering/notification'
import { ActionContext, getClient } from '@hcengineering/presentation'
import view from '@hcengineering/view'
@ -45,7 +46,6 @@
import Filter from '../Filter.svelte'
import {
archiveAll,
decodeObjectURI,
getDisplayInboxData,
isMentionNotification,
openInboxDoc,

View File

@ -30,6 +30,8 @@ import {
type WithLookup
} from '@hcengineering/core'
import notification, {
decodeObjectURI,
encodeObjectURI,
notificationId,
type ActivityInboxNotification,
type Collaborators,
@ -470,14 +472,6 @@ async function generateLocation (
}
}
export function decodeObjectURI (value: string): [Ref<Doc>, Ref<Class<Doc>>] {
return decodeURIComponent(value).split('|') as [Ref<Doc>, Ref<Class<Doc>>]
}
function encodeObjectURI (_id: Ref<Doc>, _class: Ref<Class<Doc>>): string {
return encodeURIComponent([_id, _class].join('|'))
}
export function openInboxDoc (
_id?: Ref<Doc>,
_class?: Ref<Class<Doc>>,

View File

@ -418,6 +418,8 @@ const notification = plugin(notificationId, {
Inbox: '' as IntlString,
CommonNotificationTitle: '' as IntlString,
CommonNotificationBody: '' as IntlString,
CommonNotificationChanged: '' as IntlString,
CommonNotificationChangedProperty: '' as IntlString,
ChangedCollaborators: '' as IntlString,
NewCollaborators: '' as IntlString,
RemovedCollaborators: '' as IntlString,
@ -446,4 +448,5 @@ const notification = plugin(notificationId, {
}
})
export * from './utils'
export default notification

View File

@ -0,0 +1,24 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Ref, Doc, Class } from '@hcengineering/core'
export function decodeObjectURI (value: string): [Ref<Doc>, Ref<Class<Doc>>] {
return decodeURIComponent(value).split('|') as [Ref<Doc>, Ref<Class<Doc>>]
}
export function encodeObjectURI (_id: Ref<Doc>, _class: Ref<Class<Doc>>): string {
return encodeURIComponent([_id, _class].join('|'))
}

View File

@ -55,6 +55,7 @@ import { Person, PersonAccount } from '@hcengineering/contact'
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
import { IsChannelMessage, IsDirectMessage, IsThreadMessage } from './utils'
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
/**
* @public
@ -345,22 +346,26 @@ export async function OnDirectMessageSent (originTx: Tx, control: TriggerControl
return res
}
const NOTIFICATION_BODY_SIZE = 50
/**
* @public
*/
export async function getChunterNotificationContent (_: Doc, tx: TxCUD<Doc>): Promise<NotificationContent> {
const title: IntlString = chunter.string.DirectNotificationTitle
export async function getChunterNotificationContent (
_: Doc,
tx: TxCUD<Doc>,
target: Ref<Account>,
control: TriggerControl
): Promise<NotificationContent> {
let title: IntlString = notification.string.CommonNotificationTitle
let body: IntlString = chunter.string.Message
const intlParams: Record<string, string | number> = {}
let intlParamsNotLocalized: Record<string, IntlString> | undefined
let message: string | undefined
if (tx._class === core.class.TxCollectionCUD) {
const ptx = tx as TxCollectionCUD<Doc, AttachedDoc>
if (ptx.tx._class === core.class.TxCreateDoc) {
if (ptx.tx.objectClass === chunter.class.ChatMessage) {
if (control.hierarchy.isDerived(ptx.tx.objectClass, chunter.class.ChatMessage)) {
const createTx = ptx.tx as TxCreateDoc<ChatMessage>
message = createTx.attributes.message
} else if (ptx.tx.objectClass === activity.class.ActivityReference) {
@ -372,13 +377,26 @@ export async function getChunterNotificationContent (_: Doc, tx: TxCUD<Doc>): Pr
if (message !== undefined) {
intlParams.message = stripTags(message, NOTIFICATION_BODY_SIZE)
body = chunter.string.DirectNotificationBody
body = chunter.string.MessageNotificationBody
if (control.hierarchy.isDerived(tx.objectClass, chunter.class.DirectMessage)) {
body = chunter.string.DirectNotificationBody
title = chunter.string.DirectNotificationTitle
}
}
if (control.hierarchy.isDerived(tx.objectClass, chunter.class.ChatMessage)) {
intlParamsNotLocalized = {
title: chunter.string.ThreadMessage
}
}
return {
title,
body,
intlParams
intlParams,
intlParamsNotLocalized
}
}

View File

@ -42,6 +42,7 @@
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/view": "^0.6.9",
"@hcengineering/text": "^0.6.1",
"@hcengineering/contact": "^0.6.20",
"web-push": "~3.6.7"
}

View File

@ -57,18 +57,22 @@ import notification, {
Collaborators,
CommonInboxNotification,
DocNotifyContext,
encodeObjectURI,
InboxNotification,
MentionInboxNotification,
notificationId,
NotificationType,
PushData
} from '@hcengineering/notification'
import { getMetadata, getResource, translate } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import { stripTags } from '@hcengineering/text'
import serverCore from '@hcengineering/server-core'
import serverNotification, {
getEmployee,
getPersonAccount,
getPersonAccountById
getPersonAccountById,
NOTIFICATION_BODY_SIZE
} from '@hcengineering/server-notification'
import { workbenchId } from '@hcengineering/workbench'
import webpush, { WebPushError } from 'web-push'
@ -423,7 +427,8 @@ export async function pushInboxNotifications (
await createPushFromInbox(
control,
targetUser,
docNotifyContextId,
attachedTo,
attachedToClass,
notificationData,
_class,
senderId,
@ -471,7 +476,7 @@ async function commonInboxNotificationToText (doc: Data<CommonInboxNotification>
title = await translate(doc.header, params)
}
if (doc.messageHtml != null) {
body = doc.messageHtml
body = stripTags(doc.messageHtml, NOTIFICATION_BODY_SIZE)
}
if (doc.message != null) {
body = await translate(doc.message, params)
@ -479,10 +484,41 @@ async function commonInboxNotificationToText (doc: Data<CommonInboxNotification>
return [title, body]
}
async function mentionInboxNotificationToText (
doc: Data<MentionInboxNotification>,
control: TriggerControl
): Promise<[string, string]> {
let obj = (await control.findAll(doc.mentionedInClass, { _id: doc.mentionedIn }, { limit: 1 }))[0]
if (obj !== undefined) {
if (control.hierarchy.isDerived(obj._class, chunter.class.ChatMessage)) {
obj = (
await control.findAll(
(obj as ChatMessage).attachedToClass,
{ _id: (obj as ChatMessage).attachedTo },
{ limit: 1 }
)
)[0]
}
if (obj !== undefined) {
const textPresenter = getTextPresenter(obj._class, control.hierarchy)
if (textPresenter !== undefined) {
const textPresenterFunc = await getResource(textPresenter.presenter)
const title = await textPresenterFunc(obj, control)
doc.intlParams = {
...doc.intlParams,
title
}
}
}
}
return await commonInboxNotificationToText(doc)
}
export async function createPushFromInbox (
control: TriggerControl,
targetUser: Ref<Account>,
docNotifyContextId: Ref<DocNotifyContext>,
attachedTo: Ref<Doc>,
attachedToClass: Ref<Class<Doc>>,
data: Data<InboxNotification>,
_class: Ref<Class<InboxNotification>>,
senderId: Ref<PersonAccount>,
@ -492,6 +528,8 @@ export async function createPushFromInbox (
let body: string = ''
if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) {
;[title, body] = await activityInboxNotificationToText(data as Data<ActivityInboxNotification>)
} else if (control.hierarchy.isDerived(_class, notification.class.MentionInboxNotification)) {
;[title, body] = await mentionInboxNotificationToText(data as Data<MentionInboxNotification>, control)
} else if (control.hierarchy.isDerived(_class, notification.class.CommonInboxNotification)) {
;[title, body] = await commonInboxNotificationToText(data as Data<CommonInboxNotification>)
}
@ -507,7 +545,7 @@ export async function createPushFromInbox (
}
await createPushNotification(control, targetUser, title, body, _id, senderPerson?.avatar, [
notificationId,
docNotifyContextId
encodeObjectURI(attachedTo, attachedToClass)
])
}

View File

@ -32,6 +32,7 @@ import core, {
Tx,
TxCUD,
TxMixin,
TxProcessor,
TxUpdateDoc
} from '@hcengineering/core'
import serverNotification, {
@ -318,8 +319,11 @@ async function getFallbackNotificationFullfillment (
object: Doc,
originTx: TxCUD<Doc>,
control: TriggerControl
): Promise<Record<string, string | number>> {
): Promise<NotificationContent> {
const title: IntlString = notification.string.CommonNotificationTitle
let body: IntlString = notification.string.CommonNotificationBody
const intlParams: Record<string, string | number> = {}
const intlParamsNotLocalized: Record<string, IntlString> = {}
const textPresenter = getTextPresenter(object._class, control.hierarchy)
if (textPresenter !== undefined) {
@ -335,7 +339,30 @@ async function getFallbackNotificationFullfillment (
}
}
return intlParams
const actualTx = TxProcessor.extractTx(originTx)
if (actualTx._class === core.class.TxUpdateDoc) {
const updateTx = actualTx as TxUpdateDoc<Doc>
const attributes = control.hierarchy.getAllAttributes(object._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 = notification.string.CommonNotificationChangedProperty
intlParams.newValue = (updateTx.operations as any)[attrName]?.toString()
} else {
body = notification.string.CommonNotificationChanged
}
}
break
}
}
return { title, body, intlParams, intlParamsNotLocalized }
}
function getNotificationPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): NotificationPresenter | undefined {
@ -348,12 +375,14 @@ export async function getNotificationContent (
object: Doc,
control: TriggerControl
): Promise<NotificationContent> {
let title: IntlString = notification.string.CommonNotificationTitle
let body: IntlString = notification.string.CommonNotificationBody
let intlParams: Record<string, string | number> = await getFallbackNotificationFullfillment(object, originTx, control)
let intlParamsNotLocalized: Record<string, IntlString> | undefined
let { title, body, intlParams, intlParamsNotLocalized } = await getFallbackNotificationFullfillment(
object,
originTx,
control
)
const notificationPresenter = getNotificationPresenter(object._class, control.hierarchy)
const actualTx = TxProcessor.extractTx(originTx)
const notificationPresenter = getNotificationPresenter((actualTx as TxCUD<Doc>).objectClass, control.hierarchy)
if (notificationPresenter !== undefined) {
const getFuillfillmentParams = await getResource(notificationPresenter.presenter)
const updateIntlParams = await getFuillfillmentParams(object, originTx, targetUser, control)
@ -364,7 +393,10 @@ export async function getNotificationContent (
...updateIntlParams.intlParams
}
if (updateIntlParams.intlParamsNotLocalized != null) {
intlParamsNotLocalized = updateIntlParams.intlParamsNotLocalized
intlParamsNotLocalized = {
...intlParamsNotLocalized,
...updateIntlParams.intlParamsNotLocalized
}
}
}

View File

@ -129,6 +129,8 @@ export interface NotificationPresenter extends Class<Doc> {
presenter: Resource<NotificationContentProvider>
}
export const NOTIFICATION_BODY_SIZE = 50
/**
* @public
*/

View File

@ -39,6 +39,7 @@
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-task-resources": "^0.6.0",
"@hcengineering/task": "^0.6.13",
"@hcengineering/text": "^0.6.1",

View File

@ -38,6 +38,7 @@ import { NotificationContent } from '@hcengineering/notification'
import { workbenchId } from '@hcengineering/workbench'
import { stripTags } from '@hcengineering/text'
import chunter, { ChatMessage } from '@hcengineering/chunter'
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
async function updateSubIssues (
updateTx: TxUpdateDoc<Issue>,
@ -85,7 +86,6 @@ function isSamePerson (control: TriggerControl, assignee: Ref<Person>, target: R
return assignee === targetAccount?.person
}
const NOTIFICATION_BODY_SIZE = 50
/**
* @public
*/