diff --git a/models/activity/src/actions.ts b/models/activity/src/actions.ts new file mode 100644 index 0000000000..e1b2c6a02b --- /dev/null +++ b/models/activity/src/actions.ts @@ -0,0 +1,113 @@ +import type { Builder } from '@hcengineering/model' +import view, { createAction } from '@hcengineering/model-view' + +import activity from './plugin' + +export function buildActions (builder: Builder): void { + createAction( + builder, + { + action: activity.actionImpl.AddReaction, + label: activity.string.AddReaction, + icon: activity.icon.Emoji, + input: 'focus', + category: activity.category.Activity, + target: activity.class.ActivityMessage, + inline: true, + context: { + mode: 'context', + group: 'edit' + } + }, + activity.ids.AddReactionAction + ) + + createAction( + builder, + { + action: activity.actionImpl.SaveForLater, + label: activity.string.SaveForLater, + icon: activity.icon.Bookmark, + input: 'focus', + inline: true, + actionProps: { + size: 'x-small' + }, + category: activity.category.Activity, + target: activity.class.ActivityMessage, + visibilityTester: activity.function.CanSaveForLater, + context: { + mode: 'context', + group: 'edit' + } + }, + activity.ids.SaveForLaterAction + ) + + createAction( + builder, + { + action: activity.actionImpl.RemoveFromSaved, + label: activity.string.RemoveFromLater, + icon: activity.icon.BookmarkFilled, + input: 'focus', + inline: true, + actionProps: { + iconProps: { + fill: 'var(--global-accent-TextColor)' + } + }, + category: activity.category.Activity, + target: activity.class.ActivityMessage, + visibilityTester: activity.function.CanRemoveFromSaved, + context: { + mode: 'context', + group: 'edit' + } + }, + activity.ids.RemoveFromLaterAction + ) + + createAction( + builder, + { + action: activity.actionImpl.PinMessage, + label: view.string.Pin, + icon: view.icon.Pin, + input: 'focus', + inline: true, + category: activity.category.Activity, + target: activity.class.ActivityMessage, + visibilityTester: activity.function.CanPinMessage, + context: { + mode: 'context', + group: 'edit' + } + }, + activity.ids.PinMessageAction + ) + + createAction( + builder, + { + action: activity.actionImpl.UnpinMessage, + label: view.string.Unpin, + icon: view.icon.Pin, + input: 'focus', + inline: true, + actionProps: { + iconProps: { + fill: 'var(--global-accent-TextColor)' + } + }, + category: activity.category.Activity, + target: activity.class.ActivityMessage, + visibilityTester: activity.function.CanUnpinMessage, + context: { + mode: 'context', + group: 'edit' + } + }, + activity.ids.UnpinMessageAction + ) +} diff --git a/models/activity/src/index.ts b/models/activity/src/index.ts index a89ae4aa44..81c652c5f7 100644 --- a/models/activity/src/index.ts +++ b/models/activity/src/index.ts @@ -67,12 +67,13 @@ import { } from '@hcengineering/model' import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core' import preference, { TPreference } from '@hcengineering/model-preference' -import view, { createAction } from '@hcengineering/model-view' -import notification from '@hcengineering/notification' +import view from '@hcengineering/model-view' import type { Asset, IntlString, Resource } from '@hcengineering/platform' import { type AnyComponent } from '@hcengineering/ui/src/types' import activity from './plugin' +import { buildActions } from './actions' +import { buildNotifications } from './notification' export { activityId } from '@hcengineering/activity' export { activityOperation, migrateMessagesSpace } from './migration' @@ -335,22 +336,10 @@ export function createModel (builder: Builder): void { activity.ids.ReactionAddedActivityViewlet ) - builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.ClassCollaborators, { - fields: ['createdBy', 'repliedPersons'] - }) - - builder.mixin(activity.class.DocUpdateMessage, core.class.Class, notification.mixin.ClassCollaborators, { - fields: ['createdBy', 'repliedPersons'] - }) - builder.mixin(activity.class.ActivityMessage, core.class.Class, view.mixin.ObjectPanel, { component: view.component.AttachedDocPanel }) - builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.NotificationContextPresenter, { - labelPresenter: activity.component.ActivityMessageNotificationLabel - }) - builder.mixin, IndexingConfiguration>( activity.class.DocUpdateMessage, core.class.Class, @@ -371,24 +360,6 @@ export function createModel (builder: Builder): void { } ) - builder.createDoc( - notification.class.NotificationType, - core.space.Model, - { - hidden: false, - generated: false, - label: activity.string.Reactions, - group: activity.ids.ActivityNotificationGroup, - txClasses: [core.class.TxCreateDoc], - objectClass: activity.class.Reaction, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: false - } - }, - activity.ids.AddReactionNotification - ) - builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, { domain: DOMAIN_ACTIVITY, indexes: [ @@ -405,112 +376,8 @@ export function createModel (builder: Builder): void { ] }) - createAction( - builder, - { - action: activity.actionImpl.AddReaction, - label: activity.string.AddReaction, - icon: activity.icon.Emoji, - input: 'focus', - category: activity.category.Activity, - target: activity.class.ActivityMessage, - inline: true, - context: { - mode: 'context', - group: 'edit' - } - }, - activity.ids.AddReactionAction - ) - - createAction( - builder, - { - action: activity.actionImpl.SaveForLater, - label: activity.string.SaveForLater, - icon: activity.icon.Bookmark, - input: 'focus', - inline: true, - actionProps: { - size: 'x-small' - }, - category: activity.category.Activity, - target: activity.class.ActivityMessage, - visibilityTester: activity.function.CanSaveForLater, - context: { - mode: 'context', - group: 'edit' - } - }, - activity.ids.SaveForLaterAction - ) - - createAction( - builder, - { - action: activity.actionImpl.RemoveFromSaved, - label: activity.string.RemoveFromLater, - icon: activity.icon.BookmarkFilled, - input: 'focus', - inline: true, - actionProps: { - iconProps: { - fill: 'var(--global-accent-TextColor)' - } - }, - category: activity.category.Activity, - target: activity.class.ActivityMessage, - visibilityTester: activity.function.CanRemoveFromSaved, - context: { - mode: 'context', - group: 'edit' - } - }, - activity.ids.RemoveFromLaterAction - ) - - createAction( - builder, - { - action: activity.actionImpl.PinMessage, - label: view.string.Pin, - icon: view.icon.Pin, - input: 'focus', - inline: true, - category: activity.category.Activity, - target: activity.class.ActivityMessage, - visibilityTester: activity.function.CanPinMessage, - context: { - mode: 'context', - group: 'edit' - } - }, - activity.ids.PinMessageAction - ) - - createAction( - builder, - { - action: activity.actionImpl.UnpinMessage, - label: view.string.Unpin, - icon: view.icon.Pin, - input: 'focus', - inline: true, - actionProps: { - iconProps: { - fill: 'var(--global-accent-TextColor)' - } - }, - category: activity.category.Activity, - target: activity.class.ActivityMessage, - visibilityTester: activity.function.CanUnpinMessage, - context: { - mode: 'context', - group: 'edit' - } - }, - activity.ids.UnpinMessageAction - ) + buildActions(builder) + buildNotifications(builder) } export default activity diff --git a/models/activity/src/notification.ts b/models/activity/src/notification.ts new file mode 100644 index 0000000000..521d85b8a6 --- /dev/null +++ b/models/activity/src/notification.ts @@ -0,0 +1,55 @@ +import notification from '@hcengineering/notification' +import core from '@hcengineering/core' +import { type Builder } from '@hcengineering/model' + +import activity from './plugin' + +export function buildNotifications (builder: Builder): void { + builder.createDoc( + notification.class.NotificationGroup, + core.space.Model, + { + label: activity.string.Activity, + icon: activity.icon.Activity + }, + activity.ids.ActivityNotificationGroup + ) + + builder.createDoc( + notification.class.NotificationType, + core.space.Model, + { + hidden: false, + generated: false, + label: activity.string.Reactions, + group: activity.ids.ActivityNotificationGroup, + txClasses: [core.class.TxCreateDoc], + objectClass: activity.class.Reaction, + defaultEnabled: false, + templates: { + textTemplate: '{sender} reacted to {doc}: {reaction}', + htmlTemplate: '

{sender} reacted to {doc}: {reaction}

', + subjectTemplate: 'Reaction on {doc}' + } + }, + activity.ids.AddReactionNotification + ) + + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: [activity.ids.AddReactionNotification] + }) + + builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.ClassCollaborators, { + fields: ['createdBy', 'repliedPersons'] + }) + + builder.mixin(activity.class.DocUpdateMessage, core.class.Class, notification.mixin.ClassCollaborators, { + fields: ['createdBy', 'repliedPersons'] + }) + + builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.NotificationContextPresenter, { + labelPresenter: activity.component.ActivityMessageNotificationLabel + }) +} diff --git a/models/calendar/src/index.ts b/models/calendar/src/index.ts index c3a60be016..bc3d01d9ae 100644 --- a/models/calendar/src/index.ts +++ b/models/calendar/src/index.ts @@ -231,14 +231,17 @@ export function createModel (builder: Builder): void { htmlTemplate: 'Reminder: {doc}', subjectTemplate: 'Reminder: {doc}' }, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.EmailNotification]: false - } + defaultEnabled: false }, calendar.ids.ReminderNotification ) + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: [calendar.ids.ReminderNotification] + }) + builder.createDoc( activity.class.DocUpdateMessageViewlet, core.space.Model, diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index 32319cac47..5a468e761d 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -376,15 +376,11 @@ export function createModel (builder: Builder): void { txClasses: [core.class.TxCreateDoc], objectClass: chunter.class.ChatMessage, attachedToClass: chunter.class.DirectMessage, - providers: { - [notification.providers.EmailNotification]: false, - [notification.providers.BrowserNotification]: true, - [notification.providers.PlatformNotification]: true - }, + defaultEnabled: false, group: chunter.ids.ChunterNotificationGroup, templates: { - textTemplate: '{sender} has send you a message: {doc} {data}', - htmlTemplate: '

{sender} has send you a message {doc}

{data}', + textTemplate: '{sender} has sent you a message: {doc} {message}', + htmlTemplate: '

{sender} has sent you a message {doc}

{message}', subjectTemplate: 'You have new direct message in {doc}' } }, @@ -400,11 +396,14 @@ export function createModel (builder: Builder): void { hidden: false, txClasses: [core.class.TxCreateDoc], objectClass: chunter.class.ChatMessage, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: true - }, - group: chunter.ids.ChunterNotificationGroup + attachedToClass: chunter.class.Channel, + defaultEnabled: false, + group: chunter.ids.ChunterNotificationGroup, + templates: { + textTemplate: '{sender} has sent a message in {doc}: {message}', + htmlTemplate: '

{sender} has sent a message in {doc}

{message}', + subjectTemplate: 'You have new message in {doc}' + } }, chunter.ids.ChannelNotification ) @@ -418,11 +417,13 @@ export function createModel (builder: Builder): void { hidden: false, txClasses: [core.class.TxCreateDoc], objectClass: chunter.class.ThreadMessage, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: true - }, - group: chunter.ids.ChunterNotificationGroup + defaultEnabled: false, + group: chunter.ids.ChunterNotificationGroup, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{title}' + } }, chunter.ids.ThreadNotification ) @@ -649,6 +650,18 @@ export function createModel (builder: Builder): void { filters: ['name', 'topic', 'private', 'archived', 'members'], strict: true }) + + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: [chunter.ids.DMNotification, chunter.ids.ChannelNotification, chunter.ids.ThreadNotification] + }) + + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.PushNotificationProvider, + ignoredTypes: [], + enabledTypes: [chunter.ids.DMNotification, chunter.ids.ChannelNotification, chunter.ids.ThreadNotification] + }) } export default chunter diff --git a/models/chunter/src/migration.ts b/models/chunter/src/migration.ts index 1aca624a2c..c4bb67fca2 100644 --- a/models/chunter/src/migration.ts +++ b/models/chunter/src/migration.ts @@ -34,6 +34,7 @@ import { import activity, { migrateMessagesSpace, DOMAIN_ACTIVITY } from '@hcengineering/model-activity' import notification from '@hcengineering/notification' import contactPlugin, { type PersonAccount } from '@hcengineering/contact' +import { DOMAIN_NOTIFICATION } from '@hcengineering/model-notification' import chunter from './plugin' import { DOMAIN_CHUNTER } from './index' @@ -187,6 +188,9 @@ async function removeOldClasses (client: MigrationClient): Promise { for (const _class of classes) { await client.deleteMany(DOMAIN_CHUNTER, { _class }) + await client.deleteMany(DOMAIN_ACTIVITY, { attachedToClass: _class }) + await client.deleteMany(DOMAIN_ACTIVITY, { objectClass: _class }) + await client.deleteMany(DOMAIN_NOTIFICATION, { attachedToClass: _class }) await client.deleteMany(DOMAIN_TX, { objectClass: _class }) await client.deleteMany(DOMAIN_TX, { 'tx.objectClass': _class }) } @@ -226,7 +230,7 @@ export const chunterOperation: MigrateOperation = { } }, { - state: 'remove-old-classes', + state: 'remove-old-classes-v1', func: async (client) => { await removeOldClasses(client) } diff --git a/models/controlled-documents/src/index.ts b/models/controlled-documents/src/index.ts index 3897bac4c0..7f4329f792 100644 --- a/models/controlled-documents/src/index.ts +++ b/models/controlled-documents/src/index.ts @@ -902,9 +902,11 @@ export function defineNotifications (builder: Builder): void { field: 'content', txClasses: [core.class.TxUpdateDoc], objectClass: documents.class.ControlledDocument, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: false + defaultEnabled: false, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{title}' } }, documents.notification.ContentNotification @@ -922,11 +924,7 @@ export function defineNotifications (builder: Builder): void { field: 'state', txClasses: [core.class.TxUpdateDoc], objectClass: documents.class.ControlledDocument, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: false, - [notification.providers.EmailNotification]: false - }, + defaultEnabled: false, templates: { textTemplate: '{sender} changed {doc} status', htmlTemplate: '

{sender} changed {doc} status

', @@ -948,11 +946,7 @@ export function defineNotifications (builder: Builder): void { field: 'coAuthors', txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], objectClass: documents.class.ControlledDocument, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: true, - [notification.providers.EmailNotification]: true - }, + defaultEnabled: true, templates: { textTemplate: '{sender} assigned you as a co-author of {doc}', htmlTemplate: '

{sender} assigned you as a co-author of {doc}

', @@ -962,6 +956,12 @@ export function defineNotifications (builder: Builder): void { documents.notification.CoAuthorsNotification ) + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: [documents.notification.StateNotification, documents.notification.ContentNotification] + }) + generateClassNotificationTypes( builder, documents.class.ControlledDocument, diff --git a/models/document/src/index.ts b/models/document/src/index.ts index 4a7ba8d0bc..7ffab05254 100644 --- a/models/document/src/index.ts +++ b/models/document/src/index.ts @@ -461,14 +461,22 @@ function defineDocument (builder: Builder): void { field: 'content', txClasses: [core.class.TxUpdateDoc], objectClass: document.class.Document, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: false + defaultEnabled: false, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{title}' } }, document.ids.ContentNotification ) + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: [document.ids.ContentNotification] + }) + generateClassNotificationTypes( builder, document.class.Document, diff --git a/models/gmail/package.json b/models/gmail/package.json index fa3cc42e50..d411caed0a 100644 --- a/models/gmail/package.json +++ b/models/gmail/package.json @@ -35,6 +35,7 @@ "@hcengineering/model-core": "^0.6.0", "@hcengineering/contact": "^0.6.24", "@hcengineering/model-contact": "^0.6.1", + "@hcengineering/model-love": "^0.6.0", "@hcengineering/gmail": "^0.6.22", "@hcengineering/gmail-resources": "^0.6.0", "@hcengineering/model-attachment": "^0.6.0", @@ -43,6 +44,7 @@ "@hcengineering/model-notification": "^0.6.0", "@hcengineering/view": "^0.6.13", "@hcengineering/setting": "^0.6.17", - "@hcengineering/ui": "^0.6.15" + "@hcengineering/ui": "^0.6.15", + "@hcengineering/preference": "^0.6.13" } } diff --git a/models/gmail/src/index.ts b/models/gmail/src/index.ts index 98c9fe2c04..d110623772 100644 --- a/models/gmail/src/index.ts +++ b/models/gmail/src/index.ts @@ -35,6 +35,8 @@ import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core' import notification from '@hcengineering/model-notification' import view, { createAction } from '@hcengineering/model-view' import setting from '@hcengineering/setting' +import love from '@hcengineering/model-love' + import gmail from './plugin' export { gmailId } from '@hcengineering/gmail' @@ -234,10 +236,7 @@ export function createModel (builder: Builder): void { objectClass: gmail.class.Message, group: gmail.ids.EmailNotificationGroup, allowedForAuthor: true, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: false - } + defaultEnabled: false }, gmail.ids.EmailNotification ) @@ -258,4 +257,36 @@ export function createModel (builder: Builder): void { { createdOn: -1 } ] }) + + builder.createDoc( + notification.class.NotificationProvider, + core.space.Model, + { + icon: contact.icon.Email, + label: gmail.string.Email, + description: gmail.string.EmailNotificationsDescription, + defaultEnabled: true, + canDisable: true, + depends: notification.providers.InboxNotificationProvider, + order: 30 + }, + gmail.providers.EmailNotificationProvider + ) + + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: [gmail.ids.EmailNotification] + }) + + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: gmail.providers.EmailNotificationProvider, + ignoredTypes: [ + gmail.ids.EmailNotification, + notification.ids.CollaboratoAddNotification, + love.ids.InviteNotification, + love.ids.KnockNotification + ], + enabledTypes: [] + }) } diff --git a/models/gmail/src/migration.ts b/models/gmail/src/migration.ts index b409744304..162a6c43b2 100644 --- a/models/gmail/src/migration.ts +++ b/models/gmail/src/migration.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import core, { type Ref, type Space } from '@hcengineering/core' +import core, { type Class, type Doc, type Ref, type Space } from '@hcengineering/core' import { gmailId } from '@hcengineering/gmail' import { migrateSpace, @@ -23,6 +23,24 @@ import { type MigrationUpgradeClient } from '@hcengineering/model' import { DOMAIN_GMAIL } from '.' +import notification from '@hcengineering/notification' +import { DOMAIN_PREFERENCE } from '@hcengineering/preference' + +import gmail from './plugin' + +async function migrateSettings (client: MigrationClient): Promise { + await client.update( + DOMAIN_PREFERENCE, + { + _class: 'notification:class:NotificationSetting' as Ref>, + attachedTo: 'notification:providers:EmailNotification' as Ref + }, + { + _class: notification.class.NotificationTypeSetting, + attachedTo: gmail.providers.EmailNotificationProvider + } + ) +} export const gmailOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { @@ -32,6 +50,10 @@ export const gmailOperation: MigrateOperation = { func: async (client: MigrationClient) => { await migrateSpace(client, 'gmail:space:Gmail' as Ref, core.space.Workspace, [DOMAIN_GMAIL]) } + }, + { + state: 'migrate-setting', + func: migrateSettings } ]) }, diff --git a/models/hr/src/index.ts b/models/hr/src/index.ts index 907e857fe3..9fe03b3033 100644 --- a/models/hr/src/index.ts +++ b/models/hr/src/index.ts @@ -483,11 +483,7 @@ export function createModel (builder: Builder): void { // will be created with different trigger txClasses: [], objectClass: hr.class.Request, - providers: { - [notification.providers.EmailNotification]: true, - [notification.providers.BrowserNotification]: false, - [notification.providers.PlatformNotification]: true - }, + defaultEnabled: true, templates: { textTemplate: 'New request: {doc}', htmlTemplate: 'New request: {doc}', @@ -508,11 +504,7 @@ export function createModel (builder: Builder): void { // will be created with different trigger txClasses: [], objectClass: hr.class.Request, - providers: { - [notification.providers.EmailNotification]: true, - [notification.providers.BrowserNotification]: true, - [notification.providers.PlatformNotification]: true - }, + defaultEnabled: true, templates: { textTemplate: 'Request updated: {doc}', htmlTemplate: 'Request updated: {doc}', @@ -533,11 +525,7 @@ export function createModel (builder: Builder): void { // will be created with different trigger txClasses: [], objectClass: hr.class.Request, - providers: { - [notification.providers.EmailNotification]: true, - [notification.providers.BrowserNotification]: true, - [notification.providers.PlatformNotification]: true - }, + defaultEnabled: true, templates: { textTemplate: 'Request removed: {doc}', htmlTemplate: 'Request removed: {doc}', @@ -558,11 +546,7 @@ export function createModel (builder: Builder): void { // will be created with different trigger txClasses: [], objectClass: hr.class.PublicHoliday, - providers: { - [notification.providers.EmailNotification]: true, - [notification.providers.BrowserNotification]: true, - [notification.providers.PlatformNotification]: true - }, + defaultEnabled: true, templates: { textTemplate: 'New public holiday: {doc}', htmlTemplate: 'New public holiday: {doc}', diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index c303e0a705..3dc47ab40a 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -367,11 +367,7 @@ export function createModel (builder: Builder): void { htmlTemplate: '

{doc} was assigned to you by {sender}

', subjectTemplate: '{doc} was assigned to you' }, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: true, - [notification.providers.EmailNotification]: true - } + defaultEnabled: true }, lead.ids.AssigneeNotification ) @@ -420,9 +416,11 @@ export function createModel (builder: Builder): void { txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], objectClass: lead.class.Funnel, spaceSubscribe: true, - providers: { - [notification.providers.PlatformNotification]: false, - [notification.providers.BrowserNotification]: false + defaultEnabled: false, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{title}' } }, lead.ids.LeadCreateNotification diff --git a/models/love/src/index.ts b/models/love/src/index.ts index 0725cf11f2..f0752ee818 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -187,10 +187,7 @@ export function createModel (builder: Builder): void { group: love.ids.LoveNotificationGroup, txClasses: [core.class.TxCreateDoc], objectClass: love.class.Invite, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: true - } + defaultEnabled: true }, love.ids.InviteNotification ) @@ -205,14 +202,18 @@ export function createModel (builder: Builder): void { group: love.ids.LoveNotificationGroup, txClasses: [], objectClass: love.class.JoinRequest, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: true, - [notification.providers.SoundNotification]: true - } + defaultEnabled: true }, love.ids.KnockNotification ) + + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.SoundNotificationProvider, + excludeIgnore: [love.ids.KnockNotification], + ignoredTypes: [], + enabledTypes: [] + }) + builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, { domain: DOMAIN_LOVE, disabled: [{ space: 1 }, { modifiedOn: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }] diff --git a/models/notification/package.json b/models/notification/package.json index f903da326e..11ecb5b4d5 100644 --- a/models/notification/package.json +++ b/models/notification/package.json @@ -42,6 +42,7 @@ "@hcengineering/setting": "^0.6.17", "@hcengineering/ui": "^0.6.15", "@hcengineering/view": "^0.6.13", - "@hcengineering/workbench": "^0.6.16" + "@hcengineering/workbench": "^0.6.16", + "@hcengineering/preference": "^0.6.13" } } diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 59cea8604a..e673ce41ba 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -69,15 +69,17 @@ import { type NotificationObjectPresenter, type NotificationPreferencesGroup, type NotificationPreview, - type NotificationProvider, - type NotificationSetting, type NotificationStatus, type NotificationTemplate, type NotificationType, type PushSubscription, - type PushSubscriptionKeys + type PushSubscriptionKeys, + type NotificationProvider, + type NotificationProviderSetting, + type NotificationTypeSetting, + type NotificationProviderDefaults } from '@hcengineering/notification' -import { type Asset, type IntlString, type Resource } from '@hcengineering/platform' +import { type Asset, type IntlString } from '@hcengineering/platform' import setting from '@hcengineering/setting' import { type AnyComponent, type Location } from '@hcengineering/ui/src/types' @@ -112,7 +114,7 @@ export class TBaseNotificationType extends TDoc implements BaseNotificationType generated!: boolean label!: IntlString group!: Ref - providers!: Record, boolean> + defaultEnabled!: boolean hidden!: boolean templates?: NotificationTemplate } @@ -142,20 +144,19 @@ export class TNotificationPreferencesGroup extends TDoc implements NotificationP presenter!: AnyComponent } -@Model(notification.class.NotificationProvider, core.class.Doc, DOMAIN_MODEL) -export class TNotificationProvider extends TDoc implements NotificationProvider { - label!: IntlString - depends?: Ref - onChange?: Resource<(value: boolean) => Promise> -} - -@Model(notification.class.NotificationSetting, preference.class.Preference) -export class TNotificationSetting extends TPreference implements NotificationSetting { +@Model(notification.class.NotificationTypeSetting, preference.class.Preference) +export class TNotificationTypeSetting extends TPreference implements NotificationTypeSetting { declare attachedTo: Ref type!: Ref enabled!: boolean } +@Model(notification.class.NotificationProviderSetting, preference.class.Preference) +export class TNotificationProviderSetting extends TPreference implements NotificationProviderSetting { + declare attachedTo: Ref + enabled!: boolean +} + @Mixin(notification.mixin.ClassCollaborators, core.class.Class) export class TClassCollaborators extends TClass { fields!: string[] @@ -284,6 +285,26 @@ export class TActivityNotificationViewlet extends TDoc implements ActivityNotifi presenter!: AnyComponent } +@Model(notification.class.NotificationProvider, core.class.Doc) +export class TNotificationProvider extends TDoc implements NotificationProvider { + icon!: Asset + label!: IntlString + description!: IntlString + defaultEnabled!: boolean + order!: number + depends?: Ref + ignoreAll?: boolean + canDisable!: boolean +} + +@Model(notification.class.NotificationProviderDefaults, core.class.Doc) +export class TNotificationProviderDefaults extends TDoc implements NotificationProviderDefaults { + provider!: Ref + excludeIgnore?: Ref[] + ignoredTypes!: Ref[] + enabledTypes!: Ref[] +} + export const notificationActionTemplates = template({ pinContext: { action: notification.actionImpl.PinDocNotifyContext, @@ -311,8 +332,6 @@ export function createModel (builder: Builder): void { builder.createModel( TBrowserNotification, TNotificationType, - TNotificationProvider, - TNotificationSetting, TNotificationGroup, TNotificationPreferencesGroup, TClassCollaborators, @@ -328,44 +347,11 @@ export function createModel (builder: Builder): void { TBaseNotificationType, TCommonNotificationType, TMentionInboxNotification, - TPushSubscription - ) - - builder.createDoc( - notification.class.NotificationProvider, - core.space.Model, - { - label: notification.string.Inbox - }, - notification.providers.PlatformNotification - ) - - builder.createDoc( - notification.class.NotificationProvider, - core.space.Model, - { - label: notification.string.Push, - depends: notification.providers.PlatformNotification - }, - notification.providers.BrowserNotification - ) - - builder.createDoc( - notification.class.NotificationProvider, - core.space.Model, - { - label: notification.string.Sound - }, - notification.providers.SoundNotification - ) - - builder.createDoc( - notification.class.NotificationProvider, - core.space.Model, - { - label: notification.string.EmailNotification - }, - notification.providers.EmailNotification + TPushSubscription, + TNotificationProvider, + TNotificationProviderSetting, + TNotificationTypeSetting, + TNotificationProviderDefaults ) builder.createDoc( @@ -434,9 +420,7 @@ export function createModel (builder: Builder): void { group: notification.ids.NotificationGroup, txClasses: [], objectClass: notification.mixin.Collaborators, - providers: { - [notification.providers.PlatformNotification]: true - } + defaultEnabled: true }, notification.ids.CollaboratoAddNotification ) @@ -560,14 +544,10 @@ export function createModel (builder: Builder): void { generated: false, hidden: false, group: notification.ids.NotificationGroup, - providers: { - [notification.providers.EmailNotification]: true, - [notification.providers.BrowserNotification]: true, - [notification.providers.PlatformNotification]: true - }, + defaultEnabled: true, templates: { - textTemplate: '{sender} mentioned you in {doc} {data}', - htmlTemplate: '

{sender} mentioned you in {doc}

{data}', + textTemplate: '{sender} mentioned you in {doc} {message}', + htmlTemplate: '

{sender} mentioned you in {doc}

{message}', subjectTemplate: 'You were mentioned in {doc}' } }, @@ -654,6 +634,7 @@ export function createModel (builder: Builder): void { indexes: [] } ) + builder.mixin, IndexingConfiguration>( notification.class.InboxNotification, core.class.Class, @@ -672,6 +653,73 @@ export function createModel (builder: Builder): void { indexes: [] } ) + + builder.mixin, IndexingConfiguration>( + notification.class.BrowserNotification, + core.class.Class, + core.mixin.IndexConfiguration, + { + searchDisabled: true, + indexes: [] + } + ) + + builder.createDoc(notification.class.NotificationPreferencesGroup, core.space.Model, { + label: notification.string.General, + icon: notification.icon.Notifications, + presenter: notification.component.GeneralPreferencesGroup + }) + + builder.createDoc( + notification.class.NotificationProvider, + core.space.Model, + { + icon: notification.icon.Inbox, + label: notification.string.Inbox, + description: notification.string.InboxNotificationsDescription, + defaultEnabled: true, + canDisable: false, + order: 10 + }, + notification.providers.InboxNotificationProvider + ) + + builder.createDoc( + notification.class.NotificationProvider, + core.space.Model, + { + icon: notification.icon.Notifications, + label: notification.string.Push, + description: notification.string.PushNotificationsDescription, + depends: notification.providers.InboxNotificationProvider, + defaultEnabled: true, + canDisable: true, + order: 20 + }, + notification.providers.PushNotificationProvider + ) + + builder.createDoc( + notification.class.NotificationProvider, + core.space.Model, + { + icon: notification.icon.Notifications, + label: notification.string.Sound, + description: notification.string.SoundNotificationsDescription, + depends: notification.providers.PushNotificationProvider, + defaultEnabled: true, + canDisable: true, + ignoreAll: true, + order: 25 + }, + notification.providers.SoundNotificationProvider + ) + + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.PushNotificationProvider, + ignoredTypes: [notification.ids.CollaboratoAddNotification], + enabledTypes: [] + }) } export function generateClassNotificationTypes ( @@ -687,6 +735,8 @@ export function generateClassNotificationTypes ( hierarchy.isDerived(_class, core.class.AttachedDoc) ? core.class.AttachedDoc : core.class.Doc ) const filtered = Array.from(attributes.values()).filter((p) => p.hidden !== true && p.readonly !== true) + const enabledInboxTypes: Ref[] = [] + for (const attribute of filtered) { if (ignoreKeys.includes(attribute.name)) continue const isCollection: boolean = core.class.Collection === attribute.type._class @@ -704,9 +754,11 @@ export function generateClassNotificationTypes ( objectClass, txClasses, hidden: false, - providers: { - [notification.providers.PlatformNotification]: defaultEnabled.includes(attribute.name), - [notification.providers.BrowserNotification]: false + defaultEnabled: false, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{doc} updated' }, label: attribute.label } @@ -715,5 +767,17 @@ export function generateClassNotificationTypes ( } const id = `${notification.class.NotificationType}_${_class}_${attribute.name}` as Ref builder.createDoc(notification.class.NotificationType, core.space.Model, data, id) + + if (defaultEnabled.includes(attribute.name)) { + enabledInboxTypes.push(id) + } + } + + if (enabledInboxTypes.length > 0) { + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: enabledInboxTypes + }) } } diff --git a/models/notification/src/migration.ts b/models/notification/src/migration.ts index 2463de4ddd..70d375ed88 100644 --- a/models/notification/src/migration.ts +++ b/models/notification/src/migration.ts @@ -22,6 +22,7 @@ import { type MigrationUpgradeClient } from '@hcengineering/model' import notification, { notificationId, type DocNotifyContext } from '@hcengineering/notification' +import { DOMAIN_PREFERENCE } from '@hcengineering/preference' import { DOMAIN_NOTIFICATION } from './index' @@ -67,6 +68,32 @@ export async function removeNotifications ( } } +export async function migrateSettings (client: MigrationClient): Promise { + await client.update( + DOMAIN_PREFERENCE, + { + _class: 'notification:class:NotificationSetting' as Ref>, + attachedTo: 'notification:providers:BrowserNotification' as Ref + }, + { + _class: notification.class.NotificationTypeSetting, + attachedTo: notification.providers.PushNotificationProvider + } + ) + + await client.update( + DOMAIN_PREFERENCE, + { + _class: 'notification:class:NotificationSetting' as Ref>, + attachedTo: 'notification:providers:PlatformNotification' as Ref + }, + { + _class: notification.class.NotificationTypeSetting, + attachedTo: notification.providers.InboxNotificationProvider + } + ) +} + export const notificationOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await tryMigrate(client, notificationId, [ @@ -96,6 +123,10 @@ export const notificationOperation: MigrateOperation = { DOMAIN_NOTIFICATION ]) } + }, + { + state: 'migrate-setting', + func: migrateSettings } ]) }, diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 8ff03f3d8e..03100bdb56 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -118,7 +118,7 @@ export function createModel (builder: Builder): void { }) builder.mixin(recruit.class.Applicant, core.class.Class, notification.mixin.ClassCollaborators, { - fields: ['createdBy'] + fields: ['createdBy', 'assignee'] }) builder.mixin(recruit.mixin.Candidate, core.class.Mixin, view.mixin.ObjectFactory, { @@ -1314,11 +1314,7 @@ export function createModel (builder: Builder): void { htmlTemplate: '

{doc} was assigned to you by {sender}

', subjectTemplate: '{doc} was assigned to you' }, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: true, - [notification.providers.EmailNotification]: true - } + defaultEnabled: true }, recruit.ids.AssigneeNotification ) @@ -1354,9 +1350,11 @@ export function createModel (builder: Builder): void { txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], objectClass: recruit.class.Applicant, spaceSubscribe: true, - providers: { - [notification.providers.PlatformNotification]: false, - [notification.providers.BrowserNotification]: false + defaultEnabled: false, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{title}' } }, recruit.ids.ApplicationCreateNotification diff --git a/models/recruit/src/review.ts b/models/recruit/src/review.ts index 7845452c24..0f1a8677f1 100644 --- a/models/recruit/src/review.ts +++ b/models/recruit/src/review.ts @@ -135,8 +135,11 @@ export function createReviewModel (builder: Builder): void { group: recruit.ids.ReviewNotificationGroup, txClasses: [core.class.TxCreateDoc], objectClass: recruit.class.Review, - providers: { - [notification.providers.PlatformNotification]: true + defaultEnabled: true, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{title}' } }, recruit.ids.ReviewCreateNotification diff --git a/models/request/src/index.ts b/models/request/src/index.ts index a6d0bf2ef4..8570a747bc 100644 --- a/models/request/src/index.ts +++ b/models/request/src/index.ts @@ -134,11 +134,7 @@ export function createModel (builder: Builder): void { group: request.ids.RequestNotificationGroup, label: request.string.Request, allowedForAuthor: true, - providers: { - [notification.providers.BrowserNotification]: true, - [notification.providers.PlatformNotification]: true, - [notification.providers.EmailNotification]: true - }, + defaultEnabled: true, templates: { textTemplate: '{sender} sent you a {doc}', htmlTemplate: '

{sender} sent you a {doc}

', diff --git a/models/server-activity/src/index.ts b/models/server-activity/src/index.ts index c1e4fef72d..c2569236e1 100644 --- a/models/server-activity/src/index.ts +++ b/models/server-activity/src/index.ts @@ -17,11 +17,17 @@ import { type Builder } from '@hcengineering/model' import serverCore from '@hcengineering/server-core' import core from '@hcengineering/core/src/component' import serverActivity from '@hcengineering/server-activity' +import serverNotification from '@hcengineering/server-notification' +import activity from '@hcengineering/activity' export { activityServerOperation } from './migration' export { serverActivityId } from '@hcengineering/server-activity' export function createModel (builder: Builder): void { + builder.mixin(activity.class.Reaction, core.class.Class, serverNotification.mixin.NotificationPresenter, { + presenter: serverActivity.function.ReactionNotificationContentProvider + }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverActivity.trigger.OnReactionChanged, txMatch: { diff --git a/models/server-chunter/src/index.ts b/models/server-chunter/src/index.ts index aa77305549..01b14c2aa9 100644 --- a/models/server-chunter/src/index.ts +++ b/models/server-chunter/src/index.ts @@ -24,14 +24,18 @@ import serverChunter from '@hcengineering/server-chunter' export { serverChunterId } from '@hcengineering/server-chunter' export function createModel (builder: Builder): void { - builder.mixin(chunter.class.Channel, core.class.Class, serverNotification.mixin.HTMLPresenter, { + builder.mixin(chunter.class.ChunterSpace, core.class.Class, serverNotification.mixin.HTMLPresenter, { presenter: serverChunter.function.ChannelHTMLPresenter }) - builder.mixin(chunter.class.Channel, core.class.Class, serverNotification.mixin.TextPresenter, { + builder.mixin(chunter.class.ChunterSpace, core.class.Class, serverNotification.mixin.TextPresenter, { presenter: serverChunter.function.ChannelTextPresenter }) + builder.mixin(chunter.class.ChatMessage, core.class.Class, serverNotification.mixin.TextPresenter, { + presenter: serverChunter.function.ChatMessageTextPresenter + }) + builder.mixin, ObjectDDParticipant>( chunter.class.ChatMessage, core.class.Class, diff --git a/models/server-gmail/src/index.ts b/models/server-gmail/src/index.ts index 9e777bd593..8218a09a36 100644 --- a/models/server-gmail/src/index.ts +++ b/models/server-gmail/src/index.ts @@ -34,6 +34,11 @@ export function createModel (builder: Builder): void { } ) + builder.createDoc(serverNotification.class.NotificationProviderResources, core.space.Model, { + provider: gmail.providers.EmailNotificationProvider, + fn: serverGmail.function.SendEmailNotifications + }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverGmail.trigger.OnMessageCreate, txMatch: { diff --git a/models/server-notification/src/index.ts b/models/server-notification/src/index.ts index a3323f6b18..6fc68eab55 100644 --- a/models/server-notification/src/index.ts +++ b/models/server-notification/src/index.ts @@ -14,12 +14,12 @@ // limitations under the License. // -import { type Builder, Mixin } from '@hcengineering/model' +import { type Builder, Mixin, Model } from '@hcengineering/model' import core, { type Account, type Doc, type Ref, type Tx } from '@hcengineering/core' -import { TClass } from '@hcengineering/model-core' +import { TClass, TDoc } from '@hcengineering/model-core' import { TNotificationType } from '@hcengineering/model-notification' -import notification, { type NotificationType } from '@hcengineering/notification' +import notification, { type NotificationProvider, type NotificationType } from '@hcengineering/notification' import { type Resource } from '@hcengineering/platform' import serverCore, { type TriggerControl } from '@hcengineering/server-core' import serverNotification, { @@ -28,7 +28,9 @@ import serverNotification, { type Presenter, type TextPresenter, type TypeMatch, - type NotificationContentProvider + type NotificationContentProvider, + type NotificationProviderResources, + type NotificationProviderFunc } from '@hcengineering/server-notification' export { serverNotificationId } from '@hcengineering/server-notification' @@ -55,8 +57,20 @@ export class TTypeMatch extends TNotificationType implements TypeMatch { > } +@Model(serverNotification.class.NotificationProviderResources, core.class.Doc) +export class TNotificationProviderResources extends TDoc implements NotificationProviderResources { + provider!: Ref + fn!: Resource +} + export function createModel (builder: Builder): void { - builder.createModel(THTMLPresenter, TTextPresenter, TTypeMatch, TNotificationPresenter) + builder.createModel( + THTMLPresenter, + TTextPresenter, + TTypeMatch, + TNotificationPresenter, + TNotificationProviderResources + ) builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverNotification.trigger.OnActivityNotificationViewed, diff --git a/models/setting/src/index.ts b/models/setting/src/index.ts index ec2d82b4f1..d5b8263868 100644 --- a/models/setting/src/index.ts +++ b/models/setting/src/index.ts @@ -566,10 +566,7 @@ export function createModel (builder: Builder): void { htmlTemplate: '

Integration with {doc} was disabled

', subjectTemplate: 'Integration with {doc} was disabled' }, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.EmailNotification]: true - } + defaultEnabled: true }, setting.ids.IntegrationDisabledNotification ) diff --git a/models/telegram/src/index.ts b/models/telegram/src/index.ts index 400fc9f181..2c3abd493a 100644 --- a/models/telegram/src/index.ts +++ b/models/telegram/src/index.ts @@ -199,13 +199,17 @@ export function createModel (builder: Builder): void { txClasses: [core.class.TxCreateDoc], objectClass: telegram.class.Message, group: telegram.ids.NotificationGroup, - providers: { - [notification.providers.PlatformNotification]: true - } + defaultEnabled: false }, telegram.ids.NewMessageNotification ) + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: [telegram.ids.NewMessageNotification] + }) + builder.mixin(telegram.class.Message, core.class.Class, core.mixin.FullTextSearchContext, { parentPropagate: false, childProcessingAllowed: true diff --git a/models/time/src/index.ts b/models/time/src/index.ts index 6bdc10f79b..7eebe98c1c 100644 --- a/models/time/src/index.ts +++ b/models/time/src/index.ts @@ -369,13 +369,22 @@ export function createModel (builder: Builder): void { txClasses: [core.class.TxCreateDoc], objectClass: time.class.ProjectToDo, onlyOwn: true, - providers: { - [notification.providers.PlatformNotification]: true + defaultEnabled: false, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{title}' } }, time.ids.ToDoCreated ) + builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, { + provider: notification.providers.InboxNotificationProvider, + ignoredTypes: [], + enabledTypes: [time.ids.ToDoCreated] + }) + builder.mixin(time.class.ToDo, core.class.Class, notification.mixin.ClassCollaborators, { fields: ['user'] }) diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index a848027e85..4c1e404641 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -149,11 +149,7 @@ function defineNotifications (builder: Builder): void { htmlTemplate: '

{doc} was assigned to you by {sender}

', subjectTemplate: '{doc} was assigned to you' }, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: true, - [notification.providers.EmailNotification]: true - } + defaultEnabled: true }, tracker.ids.AssigneeNotification ) diff --git a/models/training/src/index.ts b/models/training/src/index.ts index e714a7efe6..b7815b8301 100644 --- a/models/training/src/index.ts +++ b/models/training/src/index.ts @@ -587,10 +587,7 @@ function defineTrainingRequest (builder: Builder): void { group: training.notification.TrainingGroup, txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], objectClass: training.class.TrainingRequest, - providers: { - [notification.providers.EmailNotification]: true, - [notification.providers.PlatformNotification]: true - }, + defaultEnabled: true, templates: { textTemplate: '{sender} sent you a training request {doc}', htmlTemplate: '

{sender} sent you a training request {doc}

', diff --git a/packages/presentation/src/sound.ts b/packages/presentation/src/sound.ts index a326086f0a..1be62940a7 100644 --- a/packages/presentation/src/sound.ts +++ b/packages/presentation/src/sound.ts @@ -1,19 +1,26 @@ import { type Class, type Doc, type Ref } from '@hcengineering/core' -import { type Asset, getMetadata } from '@hcengineering/platform' +import { type Asset, getMetadata, getResource } from '@hcengineering/platform' import { getClient } from '.' import notification from '@hcengineering/notification' const sounds = new Map() const context = new AudioContext() + export async function prepareSound (key: string, _class?: Ref>, loop = false, play = false): Promise { - const notificationType = - _class !== undefined - ? getClient().getModel().findAllSync(notification.class.NotificationType, { objectClass: _class }) - : undefined - const notAllowed = notificationType?.[0].providers[notification.providers.SoundNotification] === false - if (notificationType === undefined || notAllowed) { - return - } + if (_class === undefined) return + + const client = getClient() + const notificationType = client + .getModel() + .findAllSync(notification.class.NotificationType, { objectClass: _class })[0] + + if (notificationType === undefined) return + + const isAllowedFn = await getResource(notification.function.IsNotificationAllowed) + const allowed: boolean = isAllowedFn(notificationType, notification.providers.SoundNotificationProvider) + + if (!allowed) return + try { const soundUrl = getMetadata(key as Asset) as string const audioBuffer = await fetch(soundUrl) diff --git a/packages/theme/styles/_layouts.scss b/packages/theme/styles/_layouts.scss index 62de28876b..5590c4fd2f 100644 --- a/packages/theme/styles/_layouts.scss +++ b/packages/theme/styles/_layouts.scss @@ -702,6 +702,7 @@ input.search { .w-32 { width: 8rem; } .w-60 { width: 15rem; } .w-85 { width: 21.25rem; } +.w-120 { width: 30rem; } .w-165 { width: 41.25rem; } .min-w-0 { min-width: 0; } .min-w-2 { min-width: .5rem; } diff --git a/plugins/activity-assets/lang/en.json b/plugins/activity-assets/lang/en.json index dfd9d563f2..af10f46378 100644 --- a/plugins/activity-assets/lang/en.json +++ b/plugins/activity-assets/lang/en.json @@ -43,6 +43,8 @@ "Thread": "Thread", "AddReaction": "Add reaction", "SaveForLater": "Save for later", - "RemoveFromLater": "Remove from saved" + "RemoveFromLater": "Remove from saved", + "ReactionNotificationTitle": "Reaction on {title}", + "ReactionNotificationBody": "{senderName}: {reaction}" } } \ No newline at end of file diff --git a/plugins/activity-assets/lang/es.json b/plugins/activity-assets/lang/es.json index 0afb157df5..12f31de626 100644 --- a/plugins/activity-assets/lang/es.json +++ b/plugins/activity-assets/lang/es.json @@ -42,6 +42,8 @@ "Thread": "Tópico", "AddReaction": "Adicionar Reacción", "SaveForLater": "Guardar para mas tarde", - "RemoveFromLater": "Remover de los Guardados" + "RemoveFromLater": "Remover de los Guardados", + "ReactionNotificationTitle": "Reacción sobre {title}", + "ReactionNotificationBody": "{senderName}: {reaction}" } } \ No newline at end of file diff --git a/plugins/activity-assets/lang/fr.json b/plugins/activity-assets/lang/fr.json index 21951e59ef..b5a444586a 100644 --- a/plugins/activity-assets/lang/fr.json +++ b/plugins/activity-assets/lang/fr.json @@ -43,6 +43,8 @@ "Thread": "Fil", "AddReaction": "Ajouter une réaction", "SaveForLater": "Enregistrer pour plus tard", - "RemoveFromLater": "Retirer des enregistrements" + "RemoveFromLater": "Retirer des enregistrements", + "ReactionNotificationTitle": "Réaction sur {title}", + "ReactionNotificationBody": "{senderName}: {reaction}" } } \ No newline at end of file diff --git a/plugins/activity-assets/lang/pt.json b/plugins/activity-assets/lang/pt.json index 923e26959d..dbc3b6fe78 100644 --- a/plugins/activity-assets/lang/pt.json +++ b/plugins/activity-assets/lang/pt.json @@ -42,6 +42,8 @@ "Thread": "Tópico", "AddReaction": "Adicionar Reação", "SaveForLater": "Guardar para mais tarde", - "RemoveFromLater": "Remover dos Guardados" + "RemoveFromLater": "Remover dos Guardados", + "ReactionNotificationTitle": "Reação em {title}", + "ReactionNotificationBody": "{senderName}: {reaction}" } } \ No newline at end of file diff --git a/plugins/activity-assets/lang/ru.json b/plugins/activity-assets/lang/ru.json index ba7a29ce60..943db2257e 100644 --- a/plugins/activity-assets/lang/ru.json +++ b/plugins/activity-assets/lang/ru.json @@ -43,6 +43,8 @@ "Thread": "Обсуждение", "AddReaction": "Добавить реакцию", "SaveForLater": "Cохранить", - "RemoveFromLater": "Удалить из сохраненных" + "RemoveFromLater": "Удалить из сохраненных", + "ReactionNotificationTitle": "Реакция на {title}", + "ReactionNotificationBody": "{senderName}: {reaction}" } } \ No newline at end of file diff --git a/plugins/activity-assets/lang/zh.json b/plugins/activity-assets/lang/zh.json index bed3022dfd..5903c17c9e 100644 --- a/plugins/activity-assets/lang/zh.json +++ b/plugins/activity-assets/lang/zh.json @@ -43,6 +43,8 @@ "Thread": "线程", "AddReaction": "添加回应", "SaveForLater": "稍后保存", - "RemoveFromLater": "从保存中移除" + "RemoveFromLater": "从保存中移除", + "ReactionNotificationTitle": "反應於 {title}", + "ReactionNotificationBody": "{senderName}: {reaction}" } } diff --git a/plugins/activity/src/index.ts b/plugins/activity/src/index.ts index ed86c445e0..746e4a9410 100644 --- a/plugins/activity/src/index.ts +++ b/plugins/activity/src/index.ts @@ -302,7 +302,9 @@ export default plugin(activityId, { Mentions: '' as IntlString, MentionedYouIn: '' as IntlString, Messages: '' as IntlString, - Thread: '' as IntlString + Thread: '' as IntlString, + ReactionNotificationTitle: '' as IntlString, + ReactionNotificationBody: '' as IntlString }, component: { Activity: '' as AnyComponent, diff --git a/plugins/attachment-resources/src/components/Attachments.svelte b/plugins/attachment-resources/src/components/Attachments.svelte index b210dcf58f..f1c1ec848d 100644 --- a/plugins/attachment-resources/src/components/Attachments.svelte +++ b/plugins/attachment-resources/src/components/Attachments.svelte @@ -28,6 +28,7 @@ import IconAttachments from './icons/Attachments.svelte' import UploadDuo from './icons/UploadDuo.svelte' + export let object: Doc | undefined = undefined export let objectId: Ref export let space: Ref export let _class: Ref> @@ -57,7 +58,7 @@ await createAttachments( client, list, - { objectClass: _class, objectId, space }, + { objectClass: object?._class ?? _class, objectId, space }, attachmentClass, attachmentClassOptions ) diff --git a/plugins/gmail-assets/lang/en.json b/plugins/gmail-assets/lang/en.json index ce3227ec13..f18b7665c4 100644 --- a/plugins/gmail-assets/lang/en.json +++ b/plugins/gmail-assets/lang/en.json @@ -37,6 +37,7 @@ "NewIncomingMessage": "Sent you a new email", "ConfigLabel": "Email", "ConfigDescription": "Extension for Gmail email integration", - "GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to Google API Services User Data Policy, including the Limited Use requirements." + "GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to Google API Services User Data Policy, including the Limited Use requirements.", + "EmailNotificationsDescription": "Receive personal notifications on email." } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/es.json b/plugins/gmail-assets/lang/es.json index ee0305b1d2..3a38407cb9 100644 --- a/plugins/gmail-assets/lang/es.json +++ b/plugins/gmail-assets/lang/es.json @@ -37,6 +37,7 @@ "NewIncomingMessage": "Te ha enviado un nuevo correo electrónico", "ConfigLabel": "Correo Electrónico", "ConfigDescription": "Extensión para la integración de correo electrónico de Gmail", - "GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to Google API Services User Data Policy, including the Limited Use requirements." + "GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to Google API Services User Data Policy, including the Limited Use requirements.", + "EmailNotificationsDescription": "Reciba notificaciones personales por correo electrónico." } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/fr.json b/plugins/gmail-assets/lang/fr.json index e369f944f6..173e4c6065 100644 --- a/plugins/gmail-assets/lang/fr.json +++ b/plugins/gmail-assets/lang/fr.json @@ -37,6 +37,7 @@ "NewIncomingMessage": "Vous a envoyé un nouvel email", "ConfigLabel": "Email", "ConfigDescription": "Extension pour l'intégration des emails Gmail", - "GooglePrivacy": "L'utilisation et le transfert des informations reçues des API Google par Huly à toute autre application respecteront les règles d'utilisation des données utilisateur des services API Google, y compris les exigences d'utilisation limitée." + "GooglePrivacy": "L'utilisation et le transfert des informations reçues des API Google par Huly à toute autre application respecteront les règles d'utilisation des données utilisateur des services API Google, y compris les exigences d'utilisation limitée.", + "EmailNotificationsDescription": "Recevez des notifications personnelles par e-mail." } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/pt.json b/plugins/gmail-assets/lang/pt.json index da85455e4e..35a2ad469c 100644 --- a/plugins/gmail-assets/lang/pt.json +++ b/plugins/gmail-assets/lang/pt.json @@ -37,6 +37,7 @@ "NewIncomingMessage": "Recebeu um novo email", "ConfigLabel": "Email", "ConfigDescription": "Extensão para a integração de email do Gmail", - "GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to Google API Services User Data Policy, including the Limited Use requirements." + "GooglePrivacy": "Huly’s use and transfer of information received from Google APIs to any other app will adhere to Google API Services User Data Policy, including the Limited Use requirements.", + "EmailNotificationsDescription": "Receba notificações pessoais por e-mail." } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/ru.json b/plugins/gmail-assets/lang/ru.json index 5d44ce097d..ecfa56d21f 100644 --- a/plugins/gmail-assets/lang/ru.json +++ b/plugins/gmail-assets/lang/ru.json @@ -37,6 +37,7 @@ "NewIncomingMessage": "Прислал вам новое сообщение", "ConfigLabel": "Электронная почта", "ConfigDescription": "Расширение по работе с Gmail электронной почтой", - "GooglePrivacy": "Использование и передача информации, полученной Huly от Google API, будет соответствовать Политике использования данных пользователей Google API, включая требования ограниченного использования." + "GooglePrivacy": "Использование и передача информации, полученной Huly от Google API, будет соответствовать Политике использования данных пользователей Google API, включая требования ограниченного использования.", + "EmailNotificationsDescription": "Получайте персональные уведомления на электронную почту." } } \ No newline at end of file diff --git a/plugins/gmail-assets/lang/zh.json b/plugins/gmail-assets/lang/zh.json index c3b6770ed7..fdbb854092 100644 --- a/plugins/gmail-assets/lang/zh.json +++ b/plugins/gmail-assets/lang/zh.json @@ -37,6 +37,7 @@ "NewIncomingMessage": "给您发送了一封新邮件", "ConfigLabel": "电子邮件", "ConfigDescription": "Gmail 邮件集成扩展", - "GooglePrivacy": "Huly 从 Google API 接收的信息的使用和传输将遵守 Google API 服务用户数据政策,包括有限使用要求。" + "GooglePrivacy": "Huly 从 Google API 接收的信息的使用和传输将遵守 Google API 服务用户数据政策,包括有限使用要求。", + "EmailNotificationsDescription": "透過電子郵件接收個人通知。" } } diff --git a/plugins/gmail/src/index.ts b/plugins/gmail/src/index.ts index b739f45986..8347b249d6 100644 --- a/plugins/gmail/src/index.ts +++ b/plugins/gmail/src/index.ts @@ -15,7 +15,7 @@ import { ChannelItem } from '@hcengineering/contact' import type { Account, AttachedDoc, Class, Doc, Ref, Timestamp } from '@hcengineering/core' -import { NotificationType } from '@hcengineering/notification' +import { NotificationProvider, NotificationType } from '@hcengineering/notification' import type { IntlString, Plugin } from '@hcengineering/platform' import { Metadata, plugin } from '@hcengineering/platform' import type { Handler, IntegrationType } from '@hcengineering/setting' @@ -89,7 +89,8 @@ export default plugin(gmailId, { }, string: { From: '' as IntlString, - To: '' as IntlString + To: '' as IntlString, + EmailNotificationsDescription: '' as IntlString }, integrationType: { Gmail: '' as Ref @@ -108,5 +109,8 @@ export default plugin(gmailId, { }, metadata: { GmailURL: '' as Metadata + }, + providers: { + EmailNotificationProvider: '' as Ref } }) diff --git a/plugins/notification-assets/lang/en.json b/plugins/notification-assets/lang/en.json index c8a8911d64..44baf53777 100644 --- a/plugins/notification-assets/lang/en.json +++ b/plugins/notification-assets/lang/en.json @@ -49,6 +49,12 @@ "Unreads": "Unreads", "EnablePush": "Enable push notifications", "NotificationBlockedInBrowser": "Notifications are blocked in your browser. Please enable notifications in your browser settings", - "Sound": "Sound" + "General": "General", + "InboxNotificationsDescription": "Receive personal notifications in your Huly inbox.", + "PushNotificationsDescription": "Receive personal notifications on desktop.", + "CommonNotificationCollectionAdded": "{senderName} added {collection}", + "CommonNotificationCollectionRemoved": "{senderName} removed {collection}", + "Sound": "Sound", + "SoundNotificationsDescription": "Receive sound notifications for events." } } diff --git a/plugins/notification-assets/lang/es.json b/plugins/notification-assets/lang/es.json index 57d99e3271..1aa45f9977 100644 --- a/plugins/notification-assets/lang/es.json +++ b/plugins/notification-assets/lang/es.json @@ -48,6 +48,12 @@ "Push": "Push", "EnablePush": "Habilitar notificaciones push", "NotificationBlockedInBrowser": "Las notificaciones están bloqueadas en tu navegador. Por favor, habilita las notificaciones en la configuración de tu navegador.", - "Sound": "Sonido" + "General": "General", + "InboxNotificationsDescription": "Reciba notificaciones personales en su bandeja de entrada de Huly.", + "PushNotificationsDescription": "Reciba notificaciones personales en el escritorio.", + "Sound": "Sonido", + "SoundNotificationsDescription": "Reciba notificaciones de sonido para eventos.", + "CommonNotificationCollectionAdded": "{senderName} añadió {collection}", + "CommonNotificationCollectionRemoved": "{senderName} eliminó {collection}" } } \ No newline at end of file diff --git a/plugins/notification-assets/lang/fr.json b/plugins/notification-assets/lang/fr.json index 107d933f7a..edd97a0431 100644 --- a/plugins/notification-assets/lang/fr.json +++ b/plugins/notification-assets/lang/fr.json @@ -49,6 +49,12 @@ "Unreads": "Non lus", "EnablePush": "Activer les notifications push", "NotificationBlockedInBrowser": "Les notifications sont bloquées dans votre navigateur. Veuillez activer les notifications dans les paramètres de votre navigateur", - "Sound": "Son" + "General": "Général", + "InboxNotificationsDescription": "Recevez des notifications personnelles dans votre boîte de réception Huly.", + "PushNotificationsDescription": "Recevez des notifications personnelles sur le bureau.", + "Sound": "Son", + "SoundNotificationsDescription": "Recevez des notifications sonores pour les événements.", + "CommonNotificationCollectionAdded": "{senderName} a ajouté {collection}", + "CommonNotificationCollectionRemoved": "{senderName} a supprimé {collection}" } } \ No newline at end of file diff --git a/plugins/notification-assets/lang/pt.json b/plugins/notification-assets/lang/pt.json index b1cad6dfc9..148544ed2c 100644 --- a/plugins/notification-assets/lang/pt.json +++ b/plugins/notification-assets/lang/pt.json @@ -48,6 +48,12 @@ "Push": "Push", "EnablePush": "Ativar notificações push", "NotificationBlockedInBrowser": "Notificações bloqueadas no navegador. Por favor habilite las notificaciones en la configuración de su navegador.", - "Sound": "Som" + "General": "Geral", + "InboxNotificationsDescription": "Receba notificações pessoais em sua caixa de entrada do Huly.", + "PushNotificationsDescription": "Receba notificações pessoais no desktop.", + "Sound": "Som", + "SoundNotificationsDescription": "Receba notificações sonoras para eventos.", + "CommonNotificationCollectionAdded": "{senderName} adicionou {collection}", + "CommonNotificationCollectionRemoved": "{senderName} removeu {collection}" } } \ No newline at end of file diff --git a/plugins/notification-assets/lang/ru.json b/plugins/notification-assets/lang/ru.json index c4e6aec3c8..e30774435e 100644 --- a/plugins/notification-assets/lang/ru.json +++ b/plugins/notification-assets/lang/ru.json @@ -49,6 +49,12 @@ "Unreads": "Непрочитанные", "EnablePush": "Включить Push-уведомления", "NotificationBlockedInBrowser": "Уведомления заблокированы в вашем браузере. Пожалуйста, включите уведомления в настройках браузера", - "Sound": "Звук" + "General": "Основное", + "InboxNotificationsDescription": "Получайте персональные уведомления на свой почтовый ящик Huly.", + "PushNotificationsDescription": "Получайте персональные уведомления на рабочий стол.", + "Sound": "Звук", + "SoundNotificationsDescription": "Получайте звуковые уведомления о событиях.", + "CommonNotificationCollectionAdded": "{senderName} добавил {collection}", + "CommonNotificationCollectionRemoved": "{senderName} удалил {collection}" } } diff --git a/plugins/notification-assets/lang/zh.json b/plugins/notification-assets/lang/zh.json index f8aa9069e1..fb2824f982 100644 --- a/plugins/notification-assets/lang/zh.json +++ b/plugins/notification-assets/lang/zh.json @@ -49,6 +49,12 @@ "Unreads": "未读", "EnablePush": "启用推送通知", "NotificationBlockedInBrowser": "通知在您的浏览器中被阻止。请在浏览器设置中启用通知", - "Sound": "声音" + "General": "通用", + "InboxNotificationsDescription": "在您的 Huly 收件匣中接收個人通知。", + "PushNotificationsDescription": "在桌面上接收個人通知。", + "Sound": "声音", + "SoundNotificationsDescription": "接收事件的声音通知。", + "CommonNotificationCollectionAdded": "{senderName} 添加了 {collection}", + "CommonNotificationCollectionRemoved": "{senderName} 移除了 {collection}" } } diff --git a/plugins/notification-resources/src/components/NotificationGroupSetting.svelte b/plugins/notification-resources/src/components/NotificationGroupSetting.svelte deleted file mode 100644 index 7d9314e7d3..0000000000 --- a/plugins/notification-resources/src/components/NotificationGroupSetting.svelte +++ /dev/null @@ -1,158 +0,0 @@ - - - -
- - {#each types as type} -
- {#if type.generated} -
- {#each providers as provider (provider._id)} - {#if type.providers[provider._id] !== undefined} -
- -
- {:else} -
- {/if} - {/each} - {/each} - -
- - diff --git a/plugins/notification-resources/src/components/settings/GeneralPreferencesGroup.svelte b/plugins/notification-resources/src/components/settings/GeneralPreferencesGroup.svelte new file mode 100644 index 0000000000..484585ae91 --- /dev/null +++ b/plugins/notification-resources/src/components/settings/GeneralPreferencesGroup.svelte @@ -0,0 +1,105 @@ + + + + +{#each providers as provider} + {@const setting = $providersSettings.find(({ attachedTo }) => attachedTo === provider._id)} + +
+
+
+ + + +
+ + +
+ {#if provider.canDisable} + onToggle(provider)} + /> + {/if} +
+{/each} + + diff --git a/plugins/notification-resources/src/components/settings/NotificationGroupSetting.svelte b/plugins/notification-resources/src/components/settings/NotificationGroupSetting.svelte new file mode 100644 index 0000000000..45e48249a9 --- /dev/null +++ b/plugins/notification-resources/src/components/settings/NotificationGroupSetting.svelte @@ -0,0 +1,194 @@ + + + +
+ + {#each types as type} +
+ {#if type.generated} +
+ {#each filteredProviders as provider (provider._id)} + {#if !isIgnored(type._id, provider)} + {@const status = getStatus(settings, type._id, provider._id)} +
+ onToggle(type._id, provider._id, !status)} + /> +
+ {:else} +
+ {/if} + {/each} + {/each} + +
+ + diff --git a/plugins/notification-resources/src/components/NotificationSettings.svelte b/plugins/notification-resources/src/components/settings/NotificationSettings.svelte similarity index 80% rename from plugins/notification-resources/src/components/NotificationSettings.svelte rename to plugins/notification-resources/src/components/settings/NotificationSettings.svelte index ab764558cf..fb82757131 100644 --- a/plugins/notification-resources/src/components/NotificationSettings.svelte +++ b/plugins/notification-resources/src/components/settings/NotificationSettings.svelte @@ -19,26 +19,28 @@ BaseNotificationType, NotificationGroup, NotificationPreferencesGroup, - NotificationSetting + NotificationTypeSetting } from '@hcengineering/notification' import { getResource } from '@hcengineering/platform' - import { createQuery, getClient } from '@hcengineering/presentation' + import { getClient } from '@hcengineering/presentation' import { - Location, - Scroller, - getCurrentResolvedLocation, - navigate, - resolvedLocationStore, - Header, Breadcrumb, defineSeparators, - settingsSeparators, - Separator, + getCurrentResolvedLocation, + Header, + Loading, + Location, + navigate, NavItem, - Loading + resolvedLocationStore, + Scroller, + Separator, + settingsSeparators } from '@hcengineering/ui' - import notification from '../plugin' + + import notification from '../../plugin' import NotificationGroupSetting from './NotificationGroupSetting.svelte' + import { providersSettings, typesSettings } from '../../utils' const client = getClient() const groups: NotificationGroup[] = client.getModel().findAllSync(notification.class.NotificationGroup, {}) @@ -46,13 +48,14 @@ .getModel() .findAllSync(notification.class.NotificationPreferencesGroup, {}) - let settings = new Map, NotificationSetting[]>() + let settings = new Map, NotificationTypeSetting[]>() - const query = createQuery() + let isProviderSettingLoading = true + let isTypeSettingLoading = true - let loading = true + $: loading = isProviderSettingLoading || isTypeSettingLoading - query.query(notification.class.NotificationSetting, {}, (res) => { + const unsubscribeTypeSetting = typesSettings.subscribe((res) => { settings = new Map() for (const value of res) { const arr = settings.get(value.type) ?? [] @@ -60,7 +63,11 @@ settings.set(value.type, arr) } settings = settings - loading = false + isTypeSettingLoading = false + }) + + const unsubscribeProviderSetting = providersSettings.subscribe(() => { + isProviderSettingLoading = false }) let group: Ref | undefined = undefined @@ -74,14 +81,18 @@ } } - onDestroy( - resolvedLocationStore.subscribe((loc) => { - void (async (loc: Location): Promise => { - group = loc.path[4] as Ref - currentPreferenceGroup = undefined - })(loc) - }) - ) + const unsubscribeLocation = resolvedLocationStore.subscribe((loc) => { + void (async (loc: Location): Promise => { + group = loc.path[4] as Ref + currentPreferenceGroup = undefined + })(loc) + }) + + onDestroy(() => { + unsubscribeLocation() + unsubscribeTypeSetting() + unsubscribeProviderSetting() + }) defineSeparators('notificationSettings', settingsSeparators) @@ -132,7 +143,7 @@
- +
diff --git a/plugins/notification-resources/src/index.ts b/plugins/notification-resources/src/index.ts index ec2f8280f3..a383a2b2f2 100644 --- a/plugins/notification-resources/src/index.ts +++ b/plugins/notification-resources/src/index.ts @@ -17,7 +17,7 @@ import { type Resources } from '@hcengineering/platform' import Inbox from './components/inbox/Inbox.svelte' -import NotificationSettings from './components/NotificationSettings.svelte' +import NotificationSettings from './components/settings/NotificationSettings.svelte' import NotificationPresenter from './components/NotificationPresenter.svelte' import DocNotifyContextPresenter from './components/DocNotifyContextPresenter.svelte' import CollaboratorsChanged from './components/activity/CollaboratorsChanged.svelte' @@ -25,6 +25,7 @@ import ActivityInboxNotificationPresenter from './components/inbox/ActivityInbox import CommonInboxNotificationPresenter from './components/inbox/CommonInboxNotificationPresenter.svelte' import NotificationCollaboratorsChanged from './components/NotificationCollaboratorsChanged.svelte' import ReactionNotificationPresenter from './components/ReactionNotificationPresenter.svelte' +import GeneralPreferencesGroup from './components/settings/GeneralPreferencesGroup.svelte' import { unsubscribe, resolveLocation, @@ -42,7 +43,8 @@ import { readAll, unreadAll, checkPermission, - unarchiveContextNotifications + unarchiveContextNotifications, + isNotificationAllowed } from './utils' import { InboxNotificationsClientImpl } from './inboxNotificationsClient' @@ -63,7 +65,8 @@ export default async (): Promise => ({ ActivityInboxNotificationPresenter, CommonInboxNotificationPresenter, NotificationCollaboratorsChanged, - ReactionNotificationPresenter + ReactionNotificationPresenter, + GeneralPreferencesGroup }, function: { // eslint-disable-next-line @typescript-eslint/unbound-method @@ -73,7 +76,8 @@ export default async (): Promise => ({ CanReadNotifyContext: canReadNotifyContext, CanUnReadNotifyContext: canUnReadNotifyContext, HasInboxNotifications: hasInboxNotifications, - CheckPushPermission: checkPermission + CheckPushPermission: checkPermission, + IsNotificationAllowed: isNotificationAllowed }, actionImpl: { Unsubscribe: unsubscribe, diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts index 3ad77b34ef..45f5de4393 100644 --- a/plugins/notification-resources/src/utils.ts +++ b/plugins/notification-resources/src/utils.ts @@ -43,9 +43,13 @@ import notification, { type DisplayInboxNotification, type DocNotifyContext, type InboxNotification, - type MentionInboxNotification + type MentionInboxNotification, + type BaseNotificationType, + type NotificationProvider, + type NotificationProviderSetting, + type NotificationTypeSetting } from '@hcengineering/notification' -import { MessageBox, getClient } from '@hcengineering/presentation' +import { MessageBox, getClient, createQuery } from '@hcengineering/presentation' import { getCurrentLocation, getLocation, @@ -65,6 +69,23 @@ import { getObjectLinkId } from '@hcengineering/view-resources' import { InboxNotificationsClientImpl } from './inboxNotificationsClient' import { type InboxData, type InboxNotificationsFilter } from './types' +export const providersSettings = writable([]) +export const typesSettings = writable([]) + +const providerSettingsQuery = createQuery(true) +const typeSettingsQuery = createQuery(true) + +export function loadNotificationSettings (): void { + providerSettingsQuery.query(notification.class.NotificationProviderSetting, {}, (res) => { + providersSettings.set(res) + }) + typeSettingsQuery.query(notification.class.NotificationTypeSetting, {}, (res) => { + typesSettings.set(res) + }) +} + +loadNotificationSettings() + export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise { if (docNotifyContext.hidden) { return false @@ -777,3 +798,39 @@ export function notificationsComparator (notifications1: InboxNotification, noti return 0 } + +export function isNotificationAllowed (type: BaseNotificationType, providerId: Ref): boolean { + const client = getClient() + const provider = client.getModel().findAllSync(notification.class.NotificationProvider, { _id: providerId })[0] + if (provider === undefined) return false + + const providerSetting = get(providersSettings).find((it) => it.attachedTo === providerId) + if (providerSetting !== undefined && !providerSetting.enabled) return false + if (providerSetting === undefined && !provider.defaultEnabled) return false + + const providerDefaults = client.getModel().findAllSync(notification.class.NotificationProviderDefaults, {}) + + if (providerDefaults.some((it) => it.provider === provider._id && it.ignoredTypes.includes(type._id))) { + return false + } + + if (provider.ignoreAll === true) { + const excludedIgnore = providerDefaults.some( + (it) => provider._id === it.provider && it.excludeIgnore !== undefined && it.excludeIgnore.includes(type._id) + ) + + if (!excludedIgnore) return false + } + + const setting = get(typesSettings).find((it) => it.attachedTo === provider._id && it.type === type._id) + + if (setting !== undefined) { + return setting.enabled + } + + if (providerDefaults.some((it) => it.provider === provider._id && it.enabledTypes.includes(type._id))) { + return true + } + + return type.defaultEnabled +} diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index ac2d0481ae..3385d2a7cc 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -124,8 +124,7 @@ export interface BaseNotificationType extends Doc { // allowed to change setting (probably we should show it, but disable toggle??) hidden: boolean group: Ref - // allowed providers and default value for it - providers: Record, boolean> + defaultEnabled: boolean // templates for email (and browser/push?) templates?: NotificationTemplate } @@ -152,19 +151,30 @@ export interface NotificationType extends BaseNotificationType { export interface CommonNotificationType extends BaseNotificationType {} -/** - * @public - */ export interface NotificationProvider extends Doc { label: IntlString + description: IntlString + icon: Asset + defaultEnabled: boolean depends?: Ref - onChange?: Resource<(value: boolean) => Promise> + canDisable: boolean + ignoreAll?: boolean + order: number } -/** - * @public - */ -export interface NotificationSetting extends Preference { +export interface NotificationProviderDefaults extends Doc { + provider: Ref + excludeIgnore?: Ref[] + ignoredTypes: Ref[] + enabledTypes: Ref[] +} + +export interface NotificationProviderSetting extends Preference { + attachedTo: Ref + enabled: boolean +} + +export interface NotificationTypeSetting extends Preference { attachedTo: Ref type: Ref enabled: boolean @@ -329,8 +339,6 @@ const notification = plugin(notificationId, { BaseNotificationType: '' as Ref>, NotificationType: '' as Ref>, CommonNotificationType: '' as Ref>, - NotificationProvider: '' as Ref>, - NotificationSetting: '' as Ref>, NotificationGroup: '' as Ref>, NotificationPreferencesGroup: '' as Ref>, DocNotifyContext: '' as Ref>, @@ -338,7 +346,11 @@ const notification = plugin(notificationId, { ActivityInboxNotification: '' as Ref>, CommonInboxNotification: '' as Ref>, ActivityNotificationViewlet: '' as Ref>, - MentionInboxNotification: '' as Ref> + MentionInboxNotification: '' as Ref>, + NotificationProvider: '' as Ref>, + NotificationTypeSetting: '' as Ref>, + NotificationProviderSetting: '' as Ref>, + NotificationProviderDefaults: '' as Ref> }, ids: { NotificationSettings: '' as Ref, @@ -350,10 +362,9 @@ const notification = plugin(notificationId, { PushPublicKey: '' as Metadata }, providers: { - PlatformNotification: '' as Ref, - BrowserNotification: '' as Ref, - EmailNotification: '' as Ref, - SoundNotification: '' as Ref + InboxNotificationProvider: '' as Ref, + PushNotificationProvider: '' as Ref, + SoundNotificationProvider: '' as Ref }, integrationType: { MobileApp: '' as Ref @@ -364,7 +375,8 @@ const notification = plugin(notificationId, { CollaboratorsChanged: '' as AnyComponent, DocNotifyContextPresenter: '' as AnyComponent, NotificationCollaboratorsChanged: '' as AnyComponent, - ReactionNotificationPresenter: '' as AnyComponent + ReactionNotificationPresenter: '' as AnyComponent, + GeneralPreferencesGroup: '' as AnyComponent }, action: { PinDocNotifyContext: '' as Ref, @@ -402,6 +414,12 @@ const notification = plugin(notificationId, { YouAddedCollaborators: '' as IntlString, YouRemovedCollaborators: '' as IntlString, Push: '' as IntlString, + General: '' as IntlString, + InboxNotificationsDescription: '' as IntlString, + PushNotificationsDescription: '' as IntlString, + CommonNotificationCollectionAdded: '' as IntlString, + CommonNotificationCollectionRemoved: '' as IntlString, + SoundNotificationsDescription: '' as IntlString, Sound: '' as IntlString }, function: { @@ -410,6 +428,9 @@ const notification = plugin(notificationId, { GetInboxNotificationsClient: '' as Resource, HasInboxNotifications: '' as Resource< (notificationsByContext: Map, InboxNotification[]>) => Promise + >, + IsNotificationAllowed: '' as Resource< + (type: BaseNotificationType, providerId: Ref) => boolean > }, resolver: { diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts index 36c8f8cea7..2f5c5718c4 100644 --- a/server-plugins/activity-resources/src/index.ts +++ b/server-plugins/activity-resources/src/index.ts @@ -36,9 +36,12 @@ import type { TriggerControl } from '@hcengineering/server-core' import { createCollabDocInfo, createCollaboratorNotifications, + getTextPresenter, removeDocInboxNotifications } from '@hcengineering/server-notification-resources' import { PersonAccount } from '@hcengineering/contact' +import { NotificationContent } from '@hcengineering/notification' +import { getResource, translate } from '@hcengineering/platform' import { getDocUpdateAction, getTxAttributesUpdates } from './utils' import { ReferenceTrigger } from './references' @@ -48,11 +51,16 @@ export async function OnReactionChanged (originTx: Tx, control: TriggerControl): const innerTx = TxProcessor.extractTx(tx) as TxCUD if (innerTx._class === core.class.TxCreateDoc) { - return await createReactionNotifications(tx, control) + const txes = await createReactionNotifications(tx, control) + + await control.apply(txes, true) + return txes } if (innerTx._class === core.class.TxRemoveDoc) { - return await removeReactionNotifications(tx, control) + const txes = await removeReactionNotifications(tx, control) + await control.apply(txes, true) + return txes } return [] @@ -411,6 +419,36 @@ async function OnDocRemoved (originTx: TxCUD, control: TriggerControl): Pro return messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id)) } +async function ReactionNotificationContentProvider ( + doc: ActivityMessage, + originTx: TxCUD, + _: Ref, + control: TriggerControl +): Promise { + const tx = TxProcessor.extractTx(originTx) as TxCreateDoc + const presenter = getTextPresenter(doc._class, control.hierarchy) + const reaction = TxProcessor.createDoc2Doc(tx) + + let text = '' + + if (presenter !== undefined) { + const fn = await getResource(presenter.presenter) + + text = await fn(doc, control) + } else { + text = await translate(activity.string.Message, {}) + } + + return { + title: activity.string.ReactionNotificationTitle, + body: activity.string.ReactionNotificationBody, + intlParams: { + title: text, + reaction: reaction.emoji + } + } +} + export * from './references' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -420,5 +458,8 @@ export default async () => ({ ActivityMessagesHandler, OnDocRemoved, OnReactionChanged + }, + function: { + ReactionNotificationContentProvider } }) diff --git a/server-plugins/activity-resources/src/references.ts b/server-plugins/activity-resources/src/references.ts index 9a65a2f6f5..1aebe4fbf2 100644 --- a/server-plugins/activity-resources/src/references.ts +++ b/server-plugins/activity-resources/src/references.ts @@ -21,6 +21,7 @@ import core, { CollaborativeDoc, Data, Doc, + generateId, Hierarchy, Ref, Space, @@ -35,7 +36,7 @@ import core, { TxUpdateDoc, Type } from '@hcengineering/core' -import notification, { MentionInboxNotification } from '@hcengineering/notification' +import notification, { CommonInboxNotification, MentionInboxNotification } from '@hcengineering/notification' import { extractReferences, markupToPmNode, @@ -52,7 +53,8 @@ import { shouldNotifyCommon, isShouldNotifyTx, NotifyResult, - createPushFromInbox + applyNotificationProviders, + getNotificationContent } from '@hcengineering/server-notification-resources' async function getPersonAccount (person: Ref, control: TriggerControl): Promise { @@ -182,57 +184,61 @@ export async function getPersonNotificationTxes ( const notifyResult = await shouldNotifyCommon(control, receiver._id, notification.ids.MentionCommonNotificationType) const messageNotifyResult = await getMessageNotifyResult(reference, receiver, control, originTx, doc) - if (messageNotifyResult.allowed) { - notifyResult.allowed = false - } - if (messageNotifyResult.push) { - notifyResult.push = false - } - if (messageNotifyResult.emails.length > 0) { - notifyResult.emails = [] - } - - const txes = await getCommonNotificationTxes( - control, - doc, - data, - receiverInfo, - senderInfo, - reference.srcDocId, - reference.srcDocClass, - space, - originTx.modifiedOn, - notifyResult, - notification.class.MentionInboxNotification - ) - - if (!notifyResult.allowed && notifyResult.push) { - const exists = ( - await control.findAll( - notification.class.ActivityInboxNotification, - { attachedTo: reference.attachedDocId as Ref, user: receiver._id }, - { limit: 1, projection: { _id: 1 } } - ) - )[0] - - if (exists !== undefined) { - const pushTx = await createPushFromInbox( - control, - receiverInfo, - reference.srcDocId, - reference.srcDocClass, - { ...data, docNotifyContext: exists.docNotifyContext }, - notification.class.MentionInboxNotification, - senderInfo, - exists._id, - new Map() - ) - if (pushTx !== undefined) { - res.push(pushTx) - } + for (const [provider] of messageNotifyResult.entries()) { + if (notifyResult.has(provider)) { + notifyResult.delete(provider) } } - res.push(...txes) + + if (notifyResult.has(notification.providers.InboxNotificationProvider)) { + const txes = await getCommonNotificationTxes( + control, + doc, + data, + receiverInfo, + senderInfo, + reference.srcDocId, + reference.srcDocClass, + space, + originTx.modifiedOn, + notifyResult, + notification.class.MentionInboxNotification + ) + res.push(...txes) + } else { + const context = ( + await control.findAll( + notification.class.DocNotifyContext, + { attachedTo: reference.srcDocId, user: receiver._id }, + { projection: { _id: 1 } } + ) + )[0] + if (context !== undefined) { + const content = await getNotificationContent(originTx, receiver, senderInfo, doc, control) + const notificationData: CommonInboxNotification = { + ...data, + ...content, + docNotifyContext: context._id, + _id: generateId(), + _class: notification.class.CommonInboxNotification, + space, + modifiedOn: originTx.modifiedOn, + modifiedBy: sender._id + } + await applyNotificationProviders( + notificationData, + notifyResult, + reference.srcDocId, + reference.srcDocClass, + control, + res, + doc, + receiverInfo, + senderInfo + ) + } + } + return res } @@ -322,17 +328,17 @@ async function getMessageNotifyResult ( reference.attachedDocId === undefined || tx._class !== core.class.TxCreateDoc ) { - return { allowed: false, emails: [], push: false } + return new Map() } const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators) if (mixin === undefined || !mixin.collaborators.includes(account._id)) { - return { allowed: false, emails: [], push: false } + return new Map() } if (!hierarchy.isDerived(reference.attachedDocClass, activity.class.ActivityMessage)) { - return { allowed: false, emails: [], push: false } + return new Map() } return await isShouldNotifyTx(control, tx, originTx, doc, account, false, false, undefined) diff --git a/server-plugins/activity/src/index.ts b/server-plugins/activity/src/index.ts index 72bcd0341b..5be21e4bb8 100644 --- a/server-plugins/activity/src/index.ts +++ b/server-plugins/activity/src/index.ts @@ -15,6 +15,7 @@ import { Plugin, Resource, plugin } from '@hcengineering/platform' import type { TriggerFunc } from '@hcengineering/server-core' +import { NotificationContentProvider } from '@hcengineering/server-notification' export * from './types' export * from './utils' @@ -33,5 +34,8 @@ export default plugin(serverActivityId, { OnDocRemoved: '' as Resource, OnReactionChanged: '' as Resource, ReferenceTrigger: '' as Resource + }, + function: { + ReactionNotificationContentProvider: '' as Resource } }) diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 8845a8d84c..0bab977747 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -37,17 +37,18 @@ import core, { TxUpdateDoc } from '@hcengineering/core' import notification, { Collaborators, NotificationContent } from '@hcengineering/notification' -import { getMetadata, IntlString } from '@hcengineering/platform' +import { getMetadata, IntlString, translate } from '@hcengineering/platform' import serverCore, { TriggerControl } from '@hcengineering/server-core' import { createCollaboratorNotifications, getDocCollaborators, getMixinTx } from '@hcengineering/server-notification-resources' -import { stripTags } from '@hcengineering/text' +import { markupToText, stripTags } from '@hcengineering/text' import { workbenchId } from '@hcengineering/workbench' import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' +import { encodeObjectURI } from '@hcengineering/view' /** * @public @@ -55,9 +56,10 @@ import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' export async function channelHTMLPresenter (doc: Doc, control: TriggerControl): Promise { const channel = doc as ChunterSpace const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? '' - const path = `${workbenchId}/${control.workspace.workspaceUrl}/${chunterId}/${channel._id}` + const path = `${workbenchId}/${control.workspace.workspaceUrl}/${chunterId}/${encodeObjectURI(channel._id, channel._class)}` const link = concatLink(front, path) - return `${channel.name}` + const name = await channelTextPresenter(channel) + return `${name}` } /** @@ -65,9 +67,18 @@ export async function channelHTMLPresenter (doc: Doc, control: TriggerControl): */ export async function channelTextPresenter (doc: Doc): Promise { const channel = doc as ChunterSpace + + if (channel._class === chunter.class.DirectMessage) { + return await translate(chunter.string.Direct, {}) + } + return `${channel.name}` } +export async function ChatMessageTextPresenter (doc: ChatMessage): Promise { + return markupToText(doc.message) +} + /** * @public */ @@ -456,6 +467,7 @@ export default async () => ({ CommentRemove, ChannelHTMLPresenter: channelHTMLPresenter, ChannelTextPresenter: channelTextPresenter, - ChunterNotificationContentProvider: getChunterNotificationContent + ChunterNotificationContentProvider: getChunterNotificationContent, + ChatMessageTextPresenter } }) diff --git a/server-plugins/chunter/src/index.ts b/server-plugins/chunter/src/index.ts index 193df6d277..3f87afd060 100644 --- a/server-plugins/chunter/src/index.ts +++ b/server-plugins/chunter/src/index.ts @@ -37,6 +37,7 @@ export default plugin(serverChunterId, { CommentRemove: '' as Resource, ChannelHTMLPresenter: '' as Resource, ChannelTextPresenter: '' as Resource, - ChunterNotificationContentProvider: '' as Resource + ChunterNotificationContentProvider: '' as Resource, + ChatMessageTextPresenter: '' as Resource } }) diff --git a/server-plugins/gmail-resources/package.json b/server-plugins/gmail-resources/package.json index ab2ac17cc8..f885922d2b 100644 --- a/server-plugins/gmail-resources/package.json +++ b/server-plugins/gmail-resources/package.json @@ -40,6 +40,8 @@ "@hcengineering/core": "^0.6.32", "@hcengineering/platform": "^0.6.11", "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-notification": "^0.6.1", + "@hcengineering/server-notification-resources": "^0.6.0", "@hcengineering/notification": "^0.6.23", "@hcengineering/contact": "^0.6.24", "@hcengineering/gmail": "^0.6.22" diff --git a/server-plugins/gmail-resources/src/index.ts b/server-plugins/gmail-resources/src/index.ts index 42cf77f7f2..992b234190 100644 --- a/server-plugins/gmail-resources/src/index.ts +++ b/server-plugins/gmail-resources/src/index.ts @@ -13,10 +13,11 @@ // limitations under the License. // -import contact, { Channel } from '@hcengineering/contact' +import contact, { Channel, formatName } from '@hcengineering/contact' import { Account, Class, + concatLink, Doc, DocumentQuery, FindOptions, @@ -29,7 +30,10 @@ import { } from '@hcengineering/core' import gmail, { Message } from '@hcengineering/gmail' import { TriggerControl } from '@hcengineering/server-core' -import notification, { NotificationType } from '@hcengineering/notification' +import notification, { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification' +import serverNotification, { NotificationProviderFunc, UserInfo } from '@hcengineering/server-notification' +import { getContentByTemplate } from '@hcengineering/server-notification-resources' +import { getMetadata } from '@hcengineering/platform' /** * @public @@ -131,6 +135,94 @@ export async function IsIncomingMessage ( return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn) } +export async function sendEmailNotification ( + text: string, + html: string, + subject: string, + receiver: string +): Promise { + try { + const sesURL = getMetadata(serverNotification.metadata.SesUrl) + if (sesURL === undefined || sesURL === '') { + console.log('Please provide email service url to enable email confirmations.') + return + } + await fetch(concatLink(sesURL, '/send'), { + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + text, + html, + subject, + to: [receiver] + }) + }) + } catch (err) { + console.log('Could not send email notification', err) + } +} + +async function notifyByEmail ( + control: TriggerControl, + type: Ref, + doc: Doc | undefined, + sender: UserInfo, + receiver: UserInfo, + data: InboxNotification +): Promise { + const account = receiver.account + + if (account === undefined) { + return + } + + const senderPerson = sender.person + const senderName = senderPerson !== undefined ? formatName(senderPerson.name, control.branding?.lastNameFirst) : '' + + const content = await getContentByTemplate(doc, senderName, type, control, '', data) + + if (content !== undefined) { + await sendEmailNotification(content.text, content.html, content.subject, account.email) + } +} + +const SendEmailNotifications: NotificationProviderFunc = async ( + control: TriggerControl, + types: BaseNotificationType[], + object: Doc, + data: InboxNotification, + receiver: UserInfo, + sender: UserInfo +): Promise => { + if (types.length === 0) { + return [] + } + + if (receiver.person === undefined) { + return [] + } + + const isEmployee = control.hierarchy.hasMixin(receiver.person, contact.mixin.Employee) + + if (!isEmployee) { + return [] + } + + const employee = control.hierarchy.as(receiver.person, contact.mixin.Employee) + + if (!employee.active) { + return [] + } + + for (const type of types) { + await notifyByEmail(control, type._id, object, sender, receiver, data) + } + + return [] +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { @@ -138,6 +230,7 @@ export default async () => ({ }, function: { IsIncomingMessage, - FindMessages + FindMessages, + SendEmailNotifications } }) diff --git a/server-plugins/gmail/src/index.ts b/server-plugins/gmail/src/index.ts index 8acb3f3ed4..cbacf260db 100644 --- a/server-plugins/gmail/src/index.ts +++ b/server-plugins/gmail/src/index.ts @@ -17,7 +17,7 @@ import type { Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { ObjectDDParticipantFunc, TriggerFunc } from '@hcengineering/server-core' -import { TypeMatchFunc } from '@hcengineering/server-notification' +import { NotificationProviderFunc, TypeMatchFunc } from '@hcengineering/server-notification' /** * @public @@ -33,6 +33,7 @@ export default plugin(serverGmailId, { }, function: { IsIncomingMessage: '' as TypeMatchFunc, - FindMessages: '' as Resource + FindMessages: '' as Resource, + SendEmailNotifications: '' as Resource } }) diff --git a/server-plugins/hr-resources/package.json b/server-plugins/hr-resources/package.json index d9f2882c8b..af1af5dae1 100644 --- a/server-plugins/hr-resources/package.json +++ b/server-plugins/hr-resources/package.json @@ -43,6 +43,8 @@ "@hcengineering/contact": "^0.6.24", "@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification-resources": "^0.6.0", + "@hcengineering/gmail": "^0.6.22", + "@hcengineering/server-gmail-resources": "^0.6.0", "@hcengineering/notification": "^0.6.23", "@hcengineering/hr": "^0.6.19" } diff --git a/server-plugins/hr-resources/src/index.ts b/server-plugins/hr-resources/src/index.ts index a8c18a6b80..b5298a4d20 100644 --- a/server-plugins/hr-resources/src/index.ts +++ b/server-plugins/hr-resources/src/index.ts @@ -40,7 +40,9 @@ import notification, { NotificationType } from '@hcengineering/notification' import { translate } from '@hcengineering/platform' import { TriggerControl } from '@hcengineering/server-core' import { getEmployee, getPersonAccountById } from '@hcengineering/server-notification' -import { getContent, isAllowed, sendEmailNotification } from '@hcengineering/server-notification-resources' +import { getContentByTemplate, isAllowed } from '@hcengineering/server-notification-resources' +import gmail from '@hcengineering/gmail' +import { sendEmailNotification } from '@hcengineering/server-gmail-resources' async function getOldDepartment ( currentTx: TxMixin | TxUpdateDoc, @@ -248,12 +250,13 @@ export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Pr ) } +// TODO: why we need specific email notifications instead of using general flow? async function sendEmailNotifications ( control: TriggerControl, sender: PersonAccount, doc: Request | PublicHoliday, space: Ref, - type: Ref + typeId: Ref ): Promise { const contacts = new Set>() const departments = await buildHierarchy(space, control) @@ -268,8 +271,14 @@ async function sendEmailNotifications ( const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { person: { $in: Array.from(contacts.values()) as Ref[] } }) + const type = await control.modelDb.findOne(notification.class.NotificationType, { _id: typeId }) + if (type === undefined) return + const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { + _id: gmail.providers.EmailNotificationProvider + }) + if (provider === undefined) return for (const account of accounts) { - const allowed = await isAllowed(control, account._id, type, notification.providers.EmailNotification) + const allowed = await isAllowed(control, account._id, type, provider) if (!allowed) { contacts.delete(account.person) } @@ -283,7 +292,7 @@ async function sendEmailNotifications ( const senderPerson = (await control.findAll(contact.class.Person, { _id: sender.person }))[0] const senderName = senderPerson !== undefined ? formatName(senderPerson.name, control.branding?.lastNameFirst) : '' - const content = await getContent(doc, senderName, type, control, '') + const content = await getContentByTemplate(doc, senderName, type._id, control, '') if (content === undefined) return for (const channel of channels) { diff --git a/server-plugins/love-resources/src/index.ts b/server-plugins/love-resources/src/index.ts index 924cb8eab8..58ac0e5e02 100644 --- a/server-plugins/love-resources/src/index.ts +++ b/server-plugins/love-resources/src/index.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import contact, { Employee, Person, PersonAccount, formatName, getName } from '@hcengineering/contact' +import contact, { Employee, Person, PersonAccount, getName, formatName } from '@hcengineering/contact' import core, { Account, Ref, @@ -25,10 +25,7 @@ import core, { TxUpdateDoc, UserStatus } from '@hcengineering/core' -import notification from '@hcengineering/notification' -import { translate } from '@hcengineering/platform' import { TriggerControl } from '@hcengineering/server-core' -import { createPushNotification, isAllowed } from '@hcengineering/server-notification-resources' import love, { Invite, JoinRequest, @@ -39,7 +36,10 @@ import love, { isOffice, loveId } from '@hcengineering/love' +import { createPushNotification, isAllowed } from '@hcengineering/server-notification-resources' +import notification from '@hcengineering/notification' import { workbenchId } from '@hcengineering/workbench' +import { translate } from '@hcengineering/platform' export async function OnEmployee (tx: Tx, control: TriggerControl): Promise { const actualTx = TxProcessor.extractTx(tx) as TxMixin @@ -259,17 +259,18 @@ export async function OnKnock (tx: Tx, control: TriggerControl): Promise { const res: Tx[] = [] const from = (await control.findAll(contact.class.Person, { _id: request.person }))[0] if (from === undefined) return [] + const type = await control.modelDb.findOne(notification.class.NotificationType, { + _id: love.ids.KnockNotification + }) + if (type === undefined) return [] + const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { + _id: notification.providers.PushNotificationProvider + }) + if (provider === undefined) return [] for (const user of roomInfo.persons) { const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: user }) if (userAcc === undefined) continue - if ( - await isAllowed( - control, - userAcc._id, - love.ids.KnockNotification, - notification.providers.BrowserNotification - ) - ) { + if (await isAllowed(control, userAcc._id, type, provider)) { const path = [workbenchId, control.workspace.workspaceUrl, loveId] const title = await translate(love.string.KnockingLabel, {}) const body = await translate(love.string.IsKnocking, { @@ -295,9 +296,15 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: target._id }) if (userAcc === undefined) return [] const from = (await control.findAll(contact.class.Person, { _id: invite.from }))[0] - if ( - await isAllowed(control, userAcc._id, love.ids.InviteNotification, notification.providers.BrowserNotification) - ) { + const type = await control.modelDb.findOne(notification.class.NotificationType, { + _id: love.ids.InviteNotification + }) + if (type === undefined) return [] + const provider = await control.modelDb.findOne(notification.class.NotificationProvider, { + _id: notification.providers.PushNotificationProvider + }) + if (provider === undefined) return [] + if (await isAllowed(control, userAcc._id, type, provider)) { const path = [workbenchId, control.workspace.workspaceUrl, loveId] const title = await translate(love.string.InivitingLabel, {}) const body = diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index 126b5b6e7a..e8c6e9e537 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -18,8 +18,6 @@ import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/acti import chunter, { ChatMessage } from '@hcengineering/chunter' import contact, { type AvatarInfo, - Employee, - formatName, getAvatarProviderId, getGravatarUrl, Person, @@ -74,7 +72,9 @@ import serverCore from '@hcengineering/server-core' import serverNotification, { getPersonAccount, getPersonAccountById, - NOTIFICATION_BODY_SIZE + NOTIFICATION_BODY_SIZE, + UserInfo, + NOTIFICATION_TITLE_SIZE } from '@hcengineering/server-notification' import serverView from '@hcengineering/server-view' import { stripTags } from '@hcengineering/text' @@ -82,7 +82,7 @@ import { encodeObjectURI } from '@hcengineering/view' import { workbenchId } from '@hcengineering/workbench' import webpush, { WebPushError } from 'web-push' -import { Content, NotifyParams, NotifyResult, UserInfo } from './types' +import { Content, NotifyParams, NotifyResult } from './types' import { getHTMLPresenter, getNotificationContent, @@ -128,45 +128,47 @@ export async function getCommonNotificationTxes ( notifyResult: NotifyResult, _class = notification.class.CommonInboxNotification ): Promise { + if (notifyResult.size === 0 || !notifyResult.has(notification.providers.InboxNotificationProvider)) { + return [] + } + const res: Tx[] = [] + const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo }) - if (notifyResult.allowed) { - const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo }) + const notificationTx = await pushInboxNotifications( + control, + res, + receiver, + attachedTo, + attachedToClass, + space, + notifyContexts, + data, + _class, + modifiedOn + ) - await pushInboxNotifications( - control, - res, - receiver, + if (notificationTx !== undefined) { + const notificationData = TxProcessor.createDoc2Doc(notificationTx) + await applyNotificationProviders( + notificationData, + notifyResult, attachedTo, attachedToClass, - space, - notifyContexts, - data, - _class, - modifiedOn, - sender, - notifyResult.push + control, + res, + doc, + receiver, + sender ) } - if (notifyResult.emails.length === 0) { - return res - } - - if (receiver.person !== undefined && control.hierarchy.isDerived(receiver.person._class, contact.mixin.Employee)) { - const emp = receiver.person as Employee - if (emp?.active) { - for (const type of notifyResult.emails) { - await notifyByEmail(control, type._id, doc, sender, receiver) - } - } - } - return res } async function getTextPart (doc: Doc, control: TriggerControl): Promise { const TextPresenter = getTextPresenter(doc._class, control.hierarchy) + console.log({ _class: doc._class, presenter: TextPresenter }) if (TextPresenter === undefined) return return await ( await getResource(TextPresenter.presenter) @@ -178,33 +180,52 @@ async function getHtmlPart (doc: Doc, control: TriggerControl): Promise = {} +): string { let res = replaceAll(template, '{sender}', sender) res = replaceAll(res, '{doc}', doc) res = replaceAll(res, '{data}', data) + + for (const key in params) { + res = replaceAll(res, `{${key}}`, params[key]) + } return res } /** * @public */ -export async function getContent ( +export async function getContentByTemplate ( doc: Doc | undefined, sender: string, type: Ref, control: TriggerControl, - data: string + data: string, + notificationData?: InboxNotification ): Promise { if (doc === undefined) return const notificationType = control.modelDb.getObject(type) + if (notificationType.templates === undefined) return const textPart = await getTextPart(doc, control) if (textPart === undefined) return - if (notificationType.templates === undefined) return - const text = fillTemplate(notificationType.templates.textTemplate, sender, textPart, data) + const params = + notificationData !== undefined + ? await getTranslatedNotificationContent(notificationData, notificationData._class, control) + : {} + + const text = fillTemplate(notificationType.templates.textTemplate, sender, textPart, data, params) const htmlPart = await getHtmlPart(doc, control) - const html = fillTemplate(notificationType.templates.htmlTemplate, sender, htmlPart ?? textPart, data) - const subject = fillTemplate(notificationType.templates.subjectTemplate, sender, textPart, data) + const html = fillTemplate(notificationType.templates.htmlTemplate, sender, htmlPart ?? textPart, data, params) + const subject = fillTemplate(notificationType.templates.subjectTemplate, sender, textPart, data, params) + + if (subject === '') return + return { text, html, @@ -212,59 +233,6 @@ export async function getContent ( } } -async function notifyByEmail ( - control: TriggerControl, - type: Ref, - doc: Doc | undefined, - sender: UserInfo, - receiver: UserInfo, - data: string = '' -): Promise { - const account = receiver.account - - if (account === undefined) { - return - } - - const senderPerson = sender.person - const senderName = senderPerson !== undefined ? formatName(senderPerson.name, control.branding?.lastNameFirst) : '' - - const content = await getContent(doc, senderName, type, control, data) - - if (content !== undefined) { - await sendEmailNotification(content.text, content.html, content.subject, account.email) - } -} - -export async function sendEmailNotification ( - text: string, - html: string, - subject: string, - receiver: string -): Promise { - try { - const sesURL = getMetadata(serverNotification.metadata.SesUrl) - if (sesURL === undefined || sesURL === '') { - console.log('Please provide email service url to enable email confirmations.') - return - } - await fetch(concatLink(sesURL, '/send'), { - method: 'post', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - text, - html, - subject, - to: [receiver] - }) - }) - } catch (err) { - console.log('Could not send email notification', err) - } -} - async function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Promise[]> { const hierarchy = control.hierarchy if (attr.type._class === core.class.RefTo) { @@ -371,11 +339,8 @@ export async function pushInboxNotifications ( data: Partial>, _class: Ref>, modifiedOn: Timestamp, - sender: UserInfo, - shouldPush: boolean, - shouldUpdateTimestamp = true, - cache: Map, Doc> = new Map, Doc>() -): Promise { + shouldUpdateTimestamp = true +): Promise | undefined> { const account = target.account if (account === undefined) { @@ -427,28 +392,14 @@ export async function pushInboxNotifications ( } const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData) res.push(notificationTx) - if (shouldPush) { - // const now = Date.now() - const pushTx = await createPushFromInbox( - control, - target, - attachedTo, - attachedToClass, - notificationData, - _class, - sender, - notificationTx.objectId, - cache - ) - // console.log('Push takes', Date.now() - now, 'ms') - if (pushTx !== undefined) { - res.push(pushTx) - } - } + + return notificationTx } } -async function activityInboxNotificationToText (doc: Data): Promise<[string, string]> { +async function activityInboxNotificationToText ( + doc: Data +): Promise<{ title: string, body: string, [key: string]: string }> { let title: string = '' let body: string = '' @@ -466,10 +417,12 @@ async function activityInboxNotificationToText (doc: Data): Promise<[string, string]> { +async function commonInboxNotificationToText ( + doc: Data +): Promise<{ title: string, body: string, [key: string]: string }> { let title: string = '' let body: string = '' @@ -492,13 +445,13 @@ async function commonInboxNotificationToText (doc: Data if (doc.message != null) { body = await translate(doc.message, params) } - return [title, body] + return { ...params, title, body } } async function mentionInboxNotificationToText ( doc: Data, control: TriggerControl -): Promise<[string, string]> { +): Promise<{ title: string, body: string, [key: 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)) { @@ -525,6 +478,22 @@ async function mentionInboxNotificationToText ( return await commonInboxNotificationToText(doc) } +async function getTranslatedNotificationContent ( + data: Data, + _class: Ref>, + control: TriggerControl +): Promise<{ title: string, body: string, [key: string]: string }> { + if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) { + return await activityInboxNotificationToText(data as Data) + } else if (control.hierarchy.isDerived(_class, notification.class.MentionInboxNotification)) { + return await mentionInboxNotificationToText(data as Data, control) + } else if (control.hierarchy.isDerived(_class, notification.class.CommonInboxNotification)) { + return await commonInboxNotificationToText(data as Data) + } + + return { title: '', body: '' } +} + export async function createPushFromInbox ( control: TriggerControl, target: UserInfo, @@ -536,15 +505,8 @@ export async function createPushFromInbox ( _id: Ref, cache: Map, Doc> = new Map, Doc>() ): Promise { - let title: string = '' - let body: string = '' - if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) { - ;[title, body] = await activityInboxNotificationToText(data as Data) - } else if (control.hierarchy.isDerived(_class, notification.class.MentionInboxNotification)) { - ;[title, body] = await mentionInboxNotificationToText(data as Data, control) - } else if (control.hierarchy.isDerived(_class, notification.class.CommonInboxNotification)) { - ;[title, body] = await commonInboxNotificationToText(data as Data) - } + const { title, body } = await getTranslatedNotificationContent(data, _class, control) + if (title === '' || body === '') { return } @@ -637,7 +599,7 @@ async function sendPushToSubscription ( try { await webpush.sendNotification(subscription, JSON.stringify(data)) } catch (err) { - console.log('Cannot send push notification to', targetUser, err) + control.ctx.info('Cannot send push notification to', { user: targetUser, err }) if (err instanceof WebPushError && err.body.includes('expired')) { const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id) await control.apply([tx]) @@ -656,39 +618,78 @@ export async function pushActivityInboxNotifications ( sender: UserInfo, object: Doc, docNotifyContexts: DocNotifyContext[], - activityMessages: ActivityMessage[], - shouldUpdateTimestamp: boolean, - shouldPush: boolean, - cache: Map, Doc> = new Map, Doc>() -): Promise { + activityMessage: ActivityMessage, + shouldUpdateTimestamp: boolean +): Promise | undefined> { if (target.account === undefined) { return } - for (const activityMessage of activityMessages) { - const content = await getNotificationContent(originTx, target.account, sender, object, control) - const data: Partial> = { - ...content, - attachedTo: activityMessage._id, - attachedToClass: activityMessage._class + const content = await getNotificationContent(originTx, target.account, sender, object, control) + const data: Partial> = { + ...content, + attachedTo: activityMessage._id, + attachedToClass: activityMessage._class + } + + return await pushInboxNotifications( + control, + res, + target, + activityMessage.attachedTo, + activityMessage.attachedToClass, + activityMessage.space, + docNotifyContexts, + data, + notification.class.ActivityInboxNotification, + activityMessage.modifiedOn, + shouldUpdateTimestamp + ) +} + +export async function applyNotificationProviders ( + data: InboxNotification, + notifyResult: NotifyResult, + attachedTo: Ref, + attachedToClass: Ref>, + control: TriggerControl, + res: Tx[], + object: Doc, + receiver: UserInfo, + sender: UserInfo +): Promise { + const resources = await control.modelDb.findAll(serverNotification.class.NotificationProviderResources, {}) + for (const [provider, types] of notifyResult.entries()) { + if (provider === notification.providers.PushNotificationProvider) { + // const now = Date.now() + const pushTx = await createPushFromInbox( + control, + receiver, + attachedTo, + attachedToClass, + data, + notification.class.ActivityInboxNotification, + sender, + data._id + ) + // console.log('Push takes', Date.now() - now, 'ms') + if (pushTx !== undefined) { + res.push(pushTx) + } + + continue } - await pushInboxNotifications( - control, - res, - target, - activityMessage.attachedTo, - activityMessage.attachedToClass, - activityMessage.space, - docNotifyContexts, - data, - notification.class.ActivityInboxNotification, - activityMessage.modifiedOn, - sender, - shouldPush, - shouldUpdateTimestamp, - cache - ) + const resource = resources.find((it) => it.provider === provider) + + if (resource === undefined) continue + + const fn = await getResource(resource.fn) + + const txes = await fn(control, types, object, data, receiver, sender) + if (txes.length > 0) { + res.push(...txes) + } } } @@ -697,14 +698,13 @@ export async function getNotificationTxes ( object: Doc, tx: TxCUD, originTx: TxCUD, - target: UserInfo, + receiver: UserInfo, sender: UserInfo, params: NotifyParams, docNotifyContexts: DocNotifyContext[], - activityMessages: ActivityMessage[], - cache: Map, Doc> + activityMessages: ActivityMessage[] ): Promise { - if (target.account === undefined) { + if (receiver.account === undefined) { return [] } @@ -717,39 +717,39 @@ export async function getNotificationTxes ( tx, originTx, object, - target.account, + receiver.account, params.isOwn, params.isSpace, docMessage ) - if (notifyResult.allowed) { - await pushActivityInboxNotifications( + if (notifyResult.has(notification.providers.InboxNotificationProvider)) { + const notificationTx = await pushActivityInboxNotifications( originTx, control, res, - target, + receiver, sender, object, docNotifyContexts, - [message], - params.shouldUpdateTimestamp, - notifyResult.push, - cache + message, + params.shouldUpdateTimestamp ) - } - if (notifyResult.emails.length === 0) { - continue - } + if (notificationTx !== undefined) { + const notificationData = TxProcessor.createDoc2Doc(notificationTx) - if (target.person !== undefined && control.hierarchy.isDerived(target.person._class, contact.mixin.Employee)) { - const emp = target.person as Employee - - if (emp?.active) { - for (const type of notifyResult.emails) { - await notifyByEmail(control, type._id, object, sender, target) - } + await applyNotificationProviders( + notificationData, + notifyResult, + message.attachedTo, + message.attachedToClass, + control, + res, + object, + receiver, + sender + ) } } } @@ -768,12 +768,11 @@ export async function createCollabDocInfo ( ): Promise { let res: Tx[] = [] - if (originTx.space === core.space.DerivedTx || collaborators.length === 0) { + if (originTx.space === core.space.DerivedTx) { return res } const docMessages = activityMessages.filter((message) => message.attachedTo === object._id) - if (docMessages.length === 0) { return res } @@ -788,6 +787,10 @@ export async function createCollabDocInfo ( } } + if (targets.size === 0) { + return res + } + const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: object._id }) @@ -803,7 +806,7 @@ export async function createCollabDocInfo ( if (info === undefined) continue res = res.concat( - await getNotificationTxes(control, object, tx, originTx, info, sender, params, notifyContexts, docMessages, cache) + await getNotificationTxes(control, object, tx, originTx, info, sender, params, notifyContexts, docMessages) ) } return res @@ -908,8 +911,7 @@ async function updateCollaboratorsMixin ( tx: TxMixin, control: TriggerControl, activityMessages: ActivityMessage[], - originTx: TxCUD, - cache: Map, Doc> + originTx: TxCUD ): Promise { const { hierarchy } = control @@ -948,17 +950,23 @@ async function updateCollaboratorsMixin ( prevCollabs = mixin !== undefined ? new Set(await getDocCollaborators(prevDoc, mixin, control)) : new Set() } + const type = await control.modelDb.findOne(notification.class.BaseNotificationType, { + _id: notification.ids.CollaboratoAddNotification + }) + + if (type === undefined) { + return res + } + + const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) + for (const collab of tx.attributes.collaborators) { if (!prevCollabs.has(collab) && tx.modifiedBy !== collab) { - if ( - await isAllowed( - control, - collab as Ref, - notification.ids.CollaboratoAddNotification, - notification.providers.PlatformNotification - ) - ) { - newCollabs.push(collab) + for (const provider of providers) { + if (await isAllowed(control, collab as Ref, type, provider)) { + newCollabs.push(collab) + break + } } } } @@ -977,19 +985,19 @@ async function updateCollaboratorsMixin ( if (target === undefined) continue - await pushActivityInboxNotifications( - originTx, - control, - res, - target, - sender, - prevDoc, - docNotifyContexts, - activityMessages, - true, - false, - cache - ) + for (const message of activityMessages) { + await pushActivityInboxNotifications( + originTx, + control, + res, + target, + sender, + prevDoc, + docNotifyContexts, + message, + true + ) + } } } } @@ -1220,8 +1228,11 @@ export async function OnAttributeCreate (tx: Tx, control: TriggerControl): Promi objectClass, txClasses, hidden: false, - providers: { - [notification.providers.PlatformNotification]: false + defaultEnabled: false, + templates: { + textTemplate: '{body}', + htmlTemplate: '

{body}

', + subjectTemplate: '{doc} updated' }, label: attribute.label } @@ -1330,13 +1341,7 @@ export async function createCollaboratorNotifications ( case core.class.TxMixin: { let res = await updateCollaboratorDoc(tx as TxUpdateDoc, control, originTx ?? tx, activityMessages, cache) res = res.concat( - await updateCollaboratorsMixin( - tx as TxMixin, - control, - activityMessages, - originTx ?? tx, - cache - ) + await updateCollaboratorsMixin(tx as TxMixin, control, activityMessages, originTx ?? tx) ) return await applyUserTxes(control, res) } diff --git a/server-plugins/notification-resources/src/types.ts b/server-plugins/notification-resources/src/types.ts index f10362c14f..9c1d44701c 100644 --- a/server-plugins/notification-resources/src/types.ts +++ b/server-plugins/notification-resources/src/types.ts @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import { BaseNotificationType } from '@hcengineering/notification' -import { Person, PersonAccount } from '@hcengineering/contact' -import { Account, Ref } from '@hcengineering/core' +import { BaseNotificationType, NotificationProvider } from '@hcengineering/notification' +import { Ref } from '@hcengineering/core' /** * @public @@ -28,20 +27,10 @@ export interface Content { /** * @public */ -export interface NotifyResult { - allowed: boolean - push: boolean - emails: BaseNotificationType[] -} +export type NotifyResult = Map, BaseNotificationType[]> export interface NotifyParams { isOwn: boolean isSpace: boolean shouldUpdateTimestamp: boolean } - -export interface UserInfo { - _id: Ref - account?: PersonAccount - person?: Person -} diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index a0df71f569..713e9cce24 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -30,23 +30,26 @@ import core, { MixinUpdate, Ref, Tx, + TxCreateDoc, TxCUD, TxMixin, TxProcessor, + TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core' import serverNotification, { getPersonAccountById, HTMLPresenter, NotificationPresenter, - TextPresenter + TextPresenter, + UserInfo } from '@hcengineering/server-notification' import { getResource, IntlString, translate } from '@hcengineering/platform' import contact, { formatName, PersonAccount } from '@hcengineering/contact' import { DocUpdateMessage } from '@hcengineering/activity' import { Analytics } from '@hcengineering/analytics' -import { UserInfo, NotifyResult } from './types' +import { NotifyResult } from './types' /** * @public @@ -107,45 +110,65 @@ export async function shouldNotifyCommon ( ): Promise { const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0] - const emailTypes: BaseNotificationType[] = [] - let allowed = false - let push = false - if (type === undefined) { - return { allowed, emails: emailTypes, push } + return new Map() } - if (await isAllowed(control, user as Ref, type._id, notification.providers.PlatformNotification)) { - allowed = true - } - if (await isAllowed(control, user as Ref, type._id, notification.providers.BrowserNotification)) { - push = true - } - if (await isAllowed(control, user as Ref, type._id, notification.providers.EmailNotification)) { - emailTypes.push(type) + const result = new Map, BaseNotificationType[]>() + const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) + + for (const provider of providers) { + const allowed = await isAllowed(control, user as Ref, type, provider) + if (allowed) { + const cur = result.get(provider._id) ?? [] + result.set(provider._id, [...cur, type]) + } } - return { allowed, push, emails: emailTypes } + return result } export async function isAllowed ( control: TriggerControl, receiver: Ref, - typeId: Ref, - providerId: Ref + type: BaseNotificationType, + provider: NotificationProvider ): Promise { - const settings = await control.queryFind(notification.class.NotificationSetting, {}) - const setting = settings.find((p) => p.attachedTo === providerId && p.type === typeId && p.modifiedBy === receiver) + const providersSettings = await control.queryFind(notification.class.NotificationProviderSetting, {}) + const providerSetting = providersSettings.find( + ({ attachedTo, modifiedBy }) => attachedTo === provider._id && modifiedBy === receiver + ) + + if (providerSetting !== undefined && !providerSetting.enabled) { + return false + } + + if (providerSetting === undefined && !provider.defaultEnabled) { + return false + } + + const providerDefaults = await control.modelDb.findAll(notification.class.NotificationProviderDefaults, {}) + + if (providerDefaults.some((it) => it.provider === provider._id && it.ignoredTypes.includes(type._id))) { + return false + } + + const typesSettings = await control.queryFind(notification.class.NotificationTypeSetting, {}) + const setting = typesSettings.find( + (it) => it.attachedTo === provider._id && it.type === type._id && it.modifiedBy === receiver + ) + if (setting !== undefined) { return setting.enabled } - const type = ( - await control.modelDb.findAll(notification.class.BaseNotificationType, { - _id: typeId - }) - )[0] + + if (providerDefaults.some((it) => it.provider === provider._id && it.enabledTypes.includes(type._id))) { + return true + } + if (type === undefined) return false - return type.providers[providerId] ?? false + + return type.defaultEnabled } export async function isShouldNotifyTx ( @@ -158,10 +181,6 @@ export async function isShouldNotifyTx ( isSpace: boolean, docUpdateMessage?: DocUpdateMessage ): Promise { - let allowed = false - let push = false - - const emailTypes: NotificationType[] = [] const types = await getMatchedTypes( control, tx, @@ -170,8 +189,9 @@ export async function isShouldNotifyTx ( isSpace, docUpdateMessage?.attributeUpdates?.attrKey ) - const modifiedAccount = await getPersonAccountById(tx.modifiedBy, control) + const result = new Map, BaseNotificationType[]>() + const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) for (const type of types) { if ( @@ -190,21 +210,17 @@ export async function isShouldNotifyTx ( if (!res) continue } } - if (await isAllowed(control, user._id, type._id, notification.providers.PlatformNotification)) { - allowed = true - } - if (await isAllowed(control, user._id, type._id, notification.providers.BrowserNotification)) { - push = true - } - if (await isAllowed(control, user._id, type._id, notification.providers.EmailNotification)) { - emailTypes.push(type) + for (const provider of providers) { + const allowed = await isAllowed(control, user._id, type, provider) + + if (allowed) { + const cur = result.get(provider._id) ?? [] + result.set(provider._id, [...cur, type]) + } } } - return { - allowed, - push, - emails: emailTypes - } + + return result } async function getMatchedTypes ( @@ -357,6 +373,24 @@ async function getFallbackNotificationFullfillment ( } break } + } else if (originTx._class === core.class.TxCollectionCUD && tx._class === core.class.TxCreateDoc) { + const createTx = tx as TxCreateDoc + const clazz = control.hierarchy.getClass(createTx.objectClass) + const label = clazz.pluralLabel ?? clazz.label + + if (label !== undefined) { + intlParamsNotLocalized.collection = clazz.pluralLabel ?? clazz.label + body = notification.string.CommonNotificationCollectionAdded + } + } else if (originTx._class === core.class.TxCollectionCUD && tx._class === core.class.TxRemoveDoc) { + const createTx = tx as TxRemoveDoc + const clazz = control.hierarchy.getClass(createTx.objectClass) + const label = clazz.pluralLabel ?? clazz.label + + if (label !== undefined) { + intlParamsNotLocalized.collection = clazz.pluralLabel ?? clazz.label + body = notification.string.CommonNotificationCollectionRemoved + } } return { title, body, intlParams, intlParamsNotLocalized } diff --git a/server-plugins/notification/src/index.ts b/server-plugins/notification/src/index.ts index 35de21e6ff..a6561a3760 100644 --- a/server-plugins/notification/src/index.ts +++ b/server-plugins/notification/src/index.ts @@ -16,7 +16,13 @@ import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' import { Account, Class, Doc, Mixin, Ref, Tx, TxCUD } from '@hcengineering/core' -import { NotificationContent, NotificationType } from '@hcengineering/notification' +import { + BaseNotificationType, + InboxNotification, + NotificationContent, + NotificationProvider, + NotificationType +} from '@hcengineering/notification' import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform' import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core' @@ -129,7 +135,28 @@ export interface NotificationPresenter extends Class { presenter: Resource } +export interface UserInfo { + _id: Ref + account?: PersonAccount + person?: Person +} + +export type NotificationProviderFunc = ( + control: TriggerControl, + types: BaseNotificationType[], + object: Doc, + data: InboxNotification, + receiver: UserInfo, + sender: UserInfo +) => Promise + +export interface NotificationProviderResources extends Doc { + provider: Ref + fn: Resource +} + export const NOTIFICATION_BODY_SIZE = 50 +export const NOTIFICATION_TITLE_SIZE = 30 /** * @public @@ -140,6 +167,9 @@ export default plugin(serverNotificationId, { PushPrivateKey: '' as Metadata, PushSubject: '' as Metadata }, + class: { + NotificationProviderResources: '' as Ref> + }, mixin: { HTMLPresenter: '' as Ref>, TextPresenter: '' as Ref>, diff --git a/server-plugins/request-resources/src/index.ts b/server-plugins/request-resources/src/index.ts index ea367fad68..9e057f86bb 100644 --- a/server-plugins/request-resources/src/index.ts +++ b/server-plugins/request-resources/src/index.ts @@ -164,8 +164,7 @@ async function getRequestNotificationTx (tx: TxCollectionCUD, cont senderInfo, { isOwn: true, isSpace: false, shouldUpdateTimestamp: true }, notifyContexts, - messages, - new Map() + messages ) res.push(...txes) } diff --git a/server-plugins/time-resources/package.json b/server-plugins/time-resources/package.json index 9c82323adc..b08270740c 100644 --- a/server-plugins/time-resources/package.json +++ b/server-plugins/time-resources/package.json @@ -42,6 +42,7 @@ "@hcengineering/notification": "^0.6.23", "@hcengineering/platform": "^0.6.11", "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification-resources": "^0.6.0", "@hcengineering/task": "^0.6.20", "@hcengineering/tracker": "^0.6.24", diff --git a/server-plugins/time-resources/src/index.ts b/server-plugins/time-resources/src/index.ts index ab49c5df9e..199a18c2e8 100644 --- a/server-plugins/time-resources/src/index.ts +++ b/server-plugins/time-resources/src/index.ts @@ -37,14 +37,14 @@ import type { TriggerControl } from '@hcengineering/server-core' import { getCommonNotificationTxes, getNotificationContent, - isShouldNotifyTx, - UserInfo + isShouldNotifyTx } from '@hcengineering/server-notification-resources' import task, { makeRank } from '@hcengineering/task' import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/text' import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker' import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time' import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time' +import { UserInfo } from '@hcengineering/server-notification' /** * @public diff --git a/tests/sanity/tests/model/profile/notifications-page.ts b/tests/sanity/tests/model/profile/notifications-page.ts index eb0b7cc7c6..a6e9d30342 100644 --- a/tests/sanity/tests/model/profile/notifications-page.ts +++ b/tests/sanity/tests/model/profile/notifications-page.ts @@ -36,7 +36,7 @@ export class NotificationsPage { documents = (): Locator => this.page.getByRole('button', { name: 'Documents' }) requests = (): Locator => this.page.getByRole('button', { name: 'Requests' }) todos = (): Locator => this.page.getByRole('button', { name: "Todo's" }) - chatMessageToggle = (): Locator => this.page.locator('div:nth-child(7) > .flex-between > .toggle > .toggle-switch') + chatMessageToggle = (): Locator => this.page.locator('.grid > div:nth-child(6)') constructor (page: Page) { this.page = page