UBERF-7513: Improve notifications model to allow external notifications channels (#6037)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-07-12 16:07:11 +04:00 committed by GitHub
parent 0fc0115c99
commit e4192b27fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 1696 additions and 931 deletions

View File

@ -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
)
}

View File

@ -67,12 +67,13 @@ import {
} from '@hcengineering/model' } from '@hcengineering/model'
import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core' import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference' import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction } from '@hcengineering/model-view' import view from '@hcengineering/model-view'
import notification from '@hcengineering/notification'
import type { Asset, IntlString, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types' import { type AnyComponent } from '@hcengineering/ui/src/types'
import activity from './plugin' import activity from './plugin'
import { buildActions } from './actions'
import { buildNotifications } from './notification'
export { activityId } from '@hcengineering/activity' export { activityId } from '@hcengineering/activity'
export { activityOperation, migrateMessagesSpace } from './migration' export { activityOperation, migrateMessagesSpace } from './migration'
@ -335,22 +336,10 @@ export function createModel (builder: Builder): void {
activity.ids.ReactionAddedActivityViewlet 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, { builder.mixin(activity.class.ActivityMessage, core.class.Class, view.mixin.ObjectPanel, {
component: view.component.AttachedDocPanel component: view.component.AttachedDocPanel
}) })
builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.NotificationContextPresenter, {
labelPresenter: activity.component.ActivityMessageNotificationLabel
})
builder.mixin<Class<DocUpdateMessage>, IndexingConfiguration<DocUpdateMessage>>( builder.mixin<Class<DocUpdateMessage>, IndexingConfiguration<DocUpdateMessage>>(
activity.class.DocUpdateMessage, activity.class.DocUpdateMessage,
core.class.Class, 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, { builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_ACTIVITY, domain: DOMAIN_ACTIVITY,
indexes: [ indexes: [
@ -405,112 +376,8 @@ export function createModel (builder: Builder): void {
] ]
}) })
createAction( buildActions(builder)
builder, buildNotifications(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
)
} }
export default activity export default activity

View File

@ -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: '<p><b>{sender}</b> reacted to {doc}: {reaction}</p>',
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
})
}

View File

@ -231,14 +231,17 @@ export function createModel (builder: Builder): void {
htmlTemplate: 'Reminder: {doc}', htmlTemplate: 'Reminder: {doc}',
subjectTemplate: 'Reminder: {doc}' subjectTemplate: 'Reminder: {doc}'
}, },
providers: { defaultEnabled: false
[notification.providers.PlatformNotification]: true,
[notification.providers.EmailNotification]: false
}
}, },
calendar.ids.ReminderNotification calendar.ids.ReminderNotification
) )
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: notification.providers.InboxNotificationProvider,
ignoredTypes: [],
enabledTypes: [calendar.ids.ReminderNotification]
})
builder.createDoc( builder.createDoc(
activity.class.DocUpdateMessageViewlet, activity.class.DocUpdateMessageViewlet,
core.space.Model, core.space.Model,

View File

@ -376,15 +376,11 @@ export function createModel (builder: Builder): void {
txClasses: [core.class.TxCreateDoc], txClasses: [core.class.TxCreateDoc],
objectClass: chunter.class.ChatMessage, objectClass: chunter.class.ChatMessage,
attachedToClass: chunter.class.DirectMessage, attachedToClass: chunter.class.DirectMessage,
providers: { defaultEnabled: false,
[notification.providers.EmailNotification]: false,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
group: chunter.ids.ChunterNotificationGroup, group: chunter.ids.ChunterNotificationGroup,
templates: { templates: {
textTemplate: '{sender} has send you a message: {doc} {data}', textTemplate: '{sender} has sent you a message: {doc} {message}',
htmlTemplate: '<p><b>{sender}</b> has send you a message {doc}</p> {data}', htmlTemplate: '<p><b>{sender}</b> has sent you a message {doc}</p> {message}',
subjectTemplate: 'You have new direct message in {doc}' subjectTemplate: 'You have new direct message in {doc}'
} }
}, },
@ -400,11 +396,14 @@ export function createModel (builder: Builder): void {
hidden: false, hidden: false,
txClasses: [core.class.TxCreateDoc], txClasses: [core.class.TxCreateDoc],
objectClass: chunter.class.ChatMessage, objectClass: chunter.class.ChatMessage,
providers: { attachedToClass: chunter.class.Channel,
[notification.providers.PlatformNotification]: true, defaultEnabled: false,
[notification.providers.BrowserNotification]: true group: chunter.ids.ChunterNotificationGroup,
}, templates: {
group: chunter.ids.ChunterNotificationGroup textTemplate: '{sender} has sent a message in {doc}: {message}',
htmlTemplate: '<p><b>{sender}</b> has sent a message in {doc}</p> {message}',
subjectTemplate: 'You have new message in {doc}'
}
}, },
chunter.ids.ChannelNotification chunter.ids.ChannelNotification
) )
@ -418,11 +417,13 @@ export function createModel (builder: Builder): void {
hidden: false, hidden: false,
txClasses: [core.class.TxCreateDoc], txClasses: [core.class.TxCreateDoc],
objectClass: chunter.class.ThreadMessage, objectClass: chunter.class.ThreadMessage,
providers: { defaultEnabled: false,
[notification.providers.PlatformNotification]: true, group: chunter.ids.ChunterNotificationGroup,
[notification.providers.BrowserNotification]: true templates: {
}, textTemplate: '{body}',
group: chunter.ids.ChunterNotificationGroup htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
}
}, },
chunter.ids.ThreadNotification chunter.ids.ThreadNotification
) )
@ -649,6 +650,18 @@ export function createModel (builder: Builder): void {
filters: ['name', 'topic', 'private', 'archived', 'members'], filters: ['name', 'topic', 'private', 'archived', 'members'],
strict: true 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 export default chunter

View File

@ -34,6 +34,7 @@ import {
import activity, { migrateMessagesSpace, DOMAIN_ACTIVITY } from '@hcengineering/model-activity' import activity, { migrateMessagesSpace, DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import contactPlugin, { type PersonAccount } from '@hcengineering/contact' import contactPlugin, { type PersonAccount } from '@hcengineering/contact'
import { DOMAIN_NOTIFICATION } from '@hcengineering/model-notification'
import chunter from './plugin' import chunter from './plugin'
import { DOMAIN_CHUNTER } from './index' import { DOMAIN_CHUNTER } from './index'
@ -187,6 +188,9 @@ async function removeOldClasses (client: MigrationClient): Promise<void> {
for (const _class of classes) { for (const _class of classes) {
await client.deleteMany(DOMAIN_CHUNTER, { _class }) 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, { objectClass: _class })
await client.deleteMany(DOMAIN_TX, { '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) => { func: async (client) => {
await removeOldClasses(client) await removeOldClasses(client)
} }

View File

@ -902,9 +902,11 @@ export function defineNotifications (builder: Builder): void {
field: 'content', field: 'content',
txClasses: [core.class.TxUpdateDoc], txClasses: [core.class.TxUpdateDoc],
objectClass: documents.class.ControlledDocument, objectClass: documents.class.ControlledDocument,
providers: { defaultEnabled: false,
[notification.providers.PlatformNotification]: true, templates: {
[notification.providers.BrowserNotification]: false textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
} }
}, },
documents.notification.ContentNotification documents.notification.ContentNotification
@ -922,11 +924,7 @@ export function defineNotifications (builder: Builder): void {
field: 'state', field: 'state',
txClasses: [core.class.TxUpdateDoc], txClasses: [core.class.TxUpdateDoc],
objectClass: documents.class.ControlledDocument, objectClass: documents.class.ControlledDocument,
providers: { defaultEnabled: false,
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: false,
[notification.providers.EmailNotification]: false
},
templates: { templates: {
textTemplate: '{sender} changed {doc} status', textTemplate: '{sender} changed {doc} status',
htmlTemplate: '<p>{sender} changed {doc} status</p>', htmlTemplate: '<p>{sender} changed {doc} status</p>',
@ -948,11 +946,7 @@ export function defineNotifications (builder: Builder): void {
field: 'coAuthors', field: 'coAuthors',
txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc],
objectClass: documents.class.ControlledDocument, objectClass: documents.class.ControlledDocument,
providers: { defaultEnabled: true,
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.EmailNotification]: true
},
templates: { templates: {
textTemplate: '{sender} assigned you as a co-author of {doc}', textTemplate: '{sender} assigned you as a co-author of {doc}',
htmlTemplate: '<p>{sender} assigned you as a co-author of {doc}</p>', htmlTemplate: '<p>{sender} assigned you as a co-author of {doc}</p>',
@ -962,6 +956,12 @@ export function defineNotifications (builder: Builder): void {
documents.notification.CoAuthorsNotification documents.notification.CoAuthorsNotification
) )
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: notification.providers.InboxNotificationProvider,
ignoredTypes: [],
enabledTypes: [documents.notification.StateNotification, documents.notification.ContentNotification]
})
generateClassNotificationTypes( generateClassNotificationTypes(
builder, builder,
documents.class.ControlledDocument, documents.class.ControlledDocument,

View File

@ -461,14 +461,22 @@ function defineDocument (builder: Builder): void {
field: 'content', field: 'content',
txClasses: [core.class.TxUpdateDoc], txClasses: [core.class.TxUpdateDoc],
objectClass: document.class.Document, objectClass: document.class.Document,
providers: { defaultEnabled: false,
[notification.providers.PlatformNotification]: true, templates: {
[notification.providers.BrowserNotification]: false textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
} }
}, },
document.ids.ContentNotification document.ids.ContentNotification
) )
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: notification.providers.InboxNotificationProvider,
ignoredTypes: [],
enabledTypes: [document.ids.ContentNotification]
})
generateClassNotificationTypes( generateClassNotificationTypes(
builder, builder,
document.class.Document, document.class.Document,

View File

@ -35,6 +35,7 @@
"@hcengineering/model-core": "^0.6.0", "@hcengineering/model-core": "^0.6.0",
"@hcengineering/contact": "^0.6.24", "@hcengineering/contact": "^0.6.24",
"@hcengineering/model-contact": "^0.6.1", "@hcengineering/model-contact": "^0.6.1",
"@hcengineering/model-love": "^0.6.0",
"@hcengineering/gmail": "^0.6.22", "@hcengineering/gmail": "^0.6.22",
"@hcengineering/gmail-resources": "^0.6.0", "@hcengineering/gmail-resources": "^0.6.0",
"@hcengineering/model-attachment": "^0.6.0", "@hcengineering/model-attachment": "^0.6.0",
@ -43,6 +44,7 @@
"@hcengineering/model-notification": "^0.6.0", "@hcengineering/model-notification": "^0.6.0",
"@hcengineering/view": "^0.6.13", "@hcengineering/view": "^0.6.13",
"@hcengineering/setting": "^0.6.17", "@hcengineering/setting": "^0.6.17",
"@hcengineering/ui": "^0.6.15" "@hcengineering/ui": "^0.6.15",
"@hcengineering/preference": "^0.6.13"
} }
} }

View File

@ -35,6 +35,8 @@ import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
import notification from '@hcengineering/model-notification' import notification from '@hcengineering/model-notification'
import view, { createAction } from '@hcengineering/model-view' import view, { createAction } from '@hcengineering/model-view'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import love from '@hcengineering/model-love'
import gmail from './plugin' import gmail from './plugin'
export { gmailId } from '@hcengineering/gmail' export { gmailId } from '@hcengineering/gmail'
@ -234,10 +236,7 @@ export function createModel (builder: Builder): void {
objectClass: gmail.class.Message, objectClass: gmail.class.Message,
group: gmail.ids.EmailNotificationGroup, group: gmail.ids.EmailNotificationGroup,
allowedForAuthor: true, allowedForAuthor: true,
providers: { defaultEnabled: false
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: false
}
}, },
gmail.ids.EmailNotification gmail.ids.EmailNotification
) )
@ -258,4 +257,36 @@ export function createModel (builder: Builder): void {
{ createdOn: -1 } { 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: []
})
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // 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 { gmailId } from '@hcengineering/gmail'
import { import {
migrateSpace, migrateSpace,
@ -23,6 +23,24 @@ import {
type MigrationUpgradeClient type MigrationUpgradeClient
} from '@hcengineering/model' } from '@hcengineering/model'
import { DOMAIN_GMAIL } from '.' 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<void> {
await client.update(
DOMAIN_PREFERENCE,
{
_class: 'notification:class:NotificationSetting' as Ref<Class<Doc>>,
attachedTo: 'notification:providers:EmailNotification' as Ref<Doc>
},
{
_class: notification.class.NotificationTypeSetting,
attachedTo: gmail.providers.EmailNotificationProvider
}
)
}
export const gmailOperation: MigrateOperation = { export const gmailOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
@ -32,6 +50,10 @@ export const gmailOperation: MigrateOperation = {
func: async (client: MigrationClient) => { func: async (client: MigrationClient) => {
await migrateSpace(client, 'gmail:space:Gmail' as Ref<Space>, core.space.Workspace, [DOMAIN_GMAIL]) await migrateSpace(client, 'gmail:space:Gmail' as Ref<Space>, core.space.Workspace, [DOMAIN_GMAIL])
} }
},
{
state: 'migrate-setting',
func: migrateSettings
} }
]) ])
}, },

View File

@ -483,11 +483,7 @@ export function createModel (builder: Builder): void {
// will be created with different trigger // will be created with different trigger
txClasses: [], txClasses: [],
objectClass: hr.class.Request, objectClass: hr.class.Request,
providers: { defaultEnabled: true,
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: false,
[notification.providers.PlatformNotification]: true
},
templates: { templates: {
textTemplate: 'New request: {doc}', textTemplate: 'New request: {doc}',
htmlTemplate: 'New request: {doc}', htmlTemplate: 'New request: {doc}',
@ -508,11 +504,7 @@ export function createModel (builder: Builder): void {
// will be created with different trigger // will be created with different trigger
txClasses: [], txClasses: [],
objectClass: hr.class.Request, objectClass: hr.class.Request,
providers: { defaultEnabled: true,
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: { templates: {
textTemplate: 'Request updated: {doc}', textTemplate: 'Request updated: {doc}',
htmlTemplate: 'Request updated: {doc}', htmlTemplate: 'Request updated: {doc}',
@ -533,11 +525,7 @@ export function createModel (builder: Builder): void {
// will be created with different trigger // will be created with different trigger
txClasses: [], txClasses: [],
objectClass: hr.class.Request, objectClass: hr.class.Request,
providers: { defaultEnabled: true,
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: { templates: {
textTemplate: 'Request removed: {doc}', textTemplate: 'Request removed: {doc}',
htmlTemplate: 'Request removed: {doc}', htmlTemplate: 'Request removed: {doc}',
@ -558,11 +546,7 @@ export function createModel (builder: Builder): void {
// will be created with different trigger // will be created with different trigger
txClasses: [], txClasses: [],
objectClass: hr.class.PublicHoliday, objectClass: hr.class.PublicHoliday,
providers: { defaultEnabled: true,
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: { templates: {
textTemplate: 'New public holiday: {doc}', textTemplate: 'New public holiday: {doc}',
htmlTemplate: 'New public holiday: {doc}', htmlTemplate: 'New public holiday: {doc}',

View File

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

View File

@ -187,10 +187,7 @@ export function createModel (builder: Builder): void {
group: love.ids.LoveNotificationGroup, group: love.ids.LoveNotificationGroup,
txClasses: [core.class.TxCreateDoc], txClasses: [core.class.TxCreateDoc],
objectClass: love.class.Invite, objectClass: love.class.Invite,
providers: { defaultEnabled: true
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true
}
}, },
love.ids.InviteNotification love.ids.InviteNotification
) )
@ -205,14 +202,18 @@ export function createModel (builder: Builder): void {
group: love.ids.LoveNotificationGroup, group: love.ids.LoveNotificationGroup,
txClasses: [], txClasses: [],
objectClass: love.class.JoinRequest, objectClass: love.class.JoinRequest,
providers: { defaultEnabled: true
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.SoundNotification]: true
}
}, },
love.ids.KnockNotification 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, { builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_LOVE, domain: DOMAIN_LOVE,
disabled: [{ space: 1 }, { modifiedOn: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }] disabled: [{ space: 1 }, { modifiedOn: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }]

View File

@ -42,6 +42,7 @@
"@hcengineering/setting": "^0.6.17", "@hcengineering/setting": "^0.6.17",
"@hcengineering/ui": "^0.6.15", "@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13", "@hcengineering/view": "^0.6.13",
"@hcengineering/workbench": "^0.6.16" "@hcengineering/workbench": "^0.6.16",
"@hcengineering/preference": "^0.6.13"
} }
} }

View File

@ -69,15 +69,17 @@ import {
type NotificationObjectPresenter, type NotificationObjectPresenter,
type NotificationPreferencesGroup, type NotificationPreferencesGroup,
type NotificationPreview, type NotificationPreview,
type NotificationProvider,
type NotificationSetting,
type NotificationStatus, type NotificationStatus,
type NotificationTemplate, type NotificationTemplate,
type NotificationType, type NotificationType,
type PushSubscription, type PushSubscription,
type PushSubscriptionKeys type PushSubscriptionKeys,
type NotificationProvider,
type NotificationProviderSetting,
type NotificationTypeSetting,
type NotificationProviderDefaults
} from '@hcengineering/notification' } 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 setting from '@hcengineering/setting'
import { type AnyComponent, type Location } from '@hcengineering/ui/src/types' import { type AnyComponent, type Location } from '@hcengineering/ui/src/types'
@ -112,7 +114,7 @@ export class TBaseNotificationType extends TDoc implements BaseNotificationType
generated!: boolean generated!: boolean
label!: IntlString label!: IntlString
group!: Ref<NotificationGroup> group!: Ref<NotificationGroup>
providers!: Record<Ref<NotificationProvider>, boolean> defaultEnabled!: boolean
hidden!: boolean hidden!: boolean
templates?: NotificationTemplate templates?: NotificationTemplate
} }
@ -142,20 +144,19 @@ export class TNotificationPreferencesGroup extends TDoc implements NotificationP
presenter!: AnyComponent presenter!: AnyComponent
} }
@Model(notification.class.NotificationProvider, core.class.Doc, DOMAIN_MODEL) @Model(notification.class.NotificationTypeSetting, preference.class.Preference)
export class TNotificationProvider extends TDoc implements NotificationProvider { export class TNotificationTypeSetting extends TPreference implements NotificationTypeSetting {
label!: IntlString
depends?: Ref<NotificationProvider>
onChange?: Resource<(value: boolean) => Promise<boolean>>
}
@Model(notification.class.NotificationSetting, preference.class.Preference)
export class TNotificationSetting extends TPreference implements NotificationSetting {
declare attachedTo: Ref<TNotificationProvider> declare attachedTo: Ref<TNotificationProvider>
type!: Ref<BaseNotificationType> type!: Ref<BaseNotificationType>
enabled!: boolean enabled!: boolean
} }
@Model(notification.class.NotificationProviderSetting, preference.class.Preference)
export class TNotificationProviderSetting extends TPreference implements NotificationProviderSetting {
declare attachedTo: Ref<TNotificationProvider>
enabled!: boolean
}
@Mixin(notification.mixin.ClassCollaborators, core.class.Class) @Mixin(notification.mixin.ClassCollaborators, core.class.Class)
export class TClassCollaborators extends TClass { export class TClassCollaborators extends TClass {
fields!: string[] fields!: string[]
@ -284,6 +285,26 @@ export class TActivityNotificationViewlet extends TDoc implements ActivityNotifi
presenter!: AnyComponent 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<NotificationProvider>
ignoreAll?: boolean
canDisable!: boolean
}
@Model(notification.class.NotificationProviderDefaults, core.class.Doc)
export class TNotificationProviderDefaults extends TDoc implements NotificationProviderDefaults {
provider!: Ref<NotificationProvider>
excludeIgnore?: Ref<BaseNotificationType>[]
ignoredTypes!: Ref<BaseNotificationType>[]
enabledTypes!: Ref<BaseNotificationType>[]
}
export const notificationActionTemplates = template({ export const notificationActionTemplates = template({
pinContext: { pinContext: {
action: notification.actionImpl.PinDocNotifyContext, action: notification.actionImpl.PinDocNotifyContext,
@ -311,8 +332,6 @@ export function createModel (builder: Builder): void {
builder.createModel( builder.createModel(
TBrowserNotification, TBrowserNotification,
TNotificationType, TNotificationType,
TNotificationProvider,
TNotificationSetting,
TNotificationGroup, TNotificationGroup,
TNotificationPreferencesGroup, TNotificationPreferencesGroup,
TClassCollaborators, TClassCollaborators,
@ -328,44 +347,11 @@ export function createModel (builder: Builder): void {
TBaseNotificationType, TBaseNotificationType,
TCommonNotificationType, TCommonNotificationType,
TMentionInboxNotification, TMentionInboxNotification,
TPushSubscription TPushSubscription,
) TNotificationProvider,
TNotificationProviderSetting,
builder.createDoc( TNotificationTypeSetting,
notification.class.NotificationProvider, TNotificationProviderDefaults
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
) )
builder.createDoc( builder.createDoc(
@ -434,9 +420,7 @@ export function createModel (builder: Builder): void {
group: notification.ids.NotificationGroup, group: notification.ids.NotificationGroup,
txClasses: [], txClasses: [],
objectClass: notification.mixin.Collaborators, objectClass: notification.mixin.Collaborators,
providers: { defaultEnabled: true
[notification.providers.PlatformNotification]: true
}
}, },
notification.ids.CollaboratoAddNotification notification.ids.CollaboratoAddNotification
) )
@ -560,14 +544,10 @@ export function createModel (builder: Builder): void {
generated: false, generated: false,
hidden: false, hidden: false,
group: notification.ids.NotificationGroup, group: notification.ids.NotificationGroup,
providers: { defaultEnabled: true,
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: { templates: {
textTemplate: '{sender} mentioned you in {doc} {data}', textTemplate: '{sender} mentioned you in {doc} {message}',
htmlTemplate: '<p>{sender}</b> mentioned you in {doc}</p> {data}', htmlTemplate: '<p>{sender}</b> mentioned you in {doc}</p> {message}',
subjectTemplate: 'You were mentioned in {doc}' subjectTemplate: 'You were mentioned in {doc}'
} }
}, },
@ -654,6 +634,7 @@ export function createModel (builder: Builder): void {
indexes: [] indexes: []
} }
) )
builder.mixin<Class<InboxNotification>, IndexingConfiguration<InboxNotification>>( builder.mixin<Class<InboxNotification>, IndexingConfiguration<InboxNotification>>(
notification.class.InboxNotification, notification.class.InboxNotification,
core.class.Class, core.class.Class,
@ -672,6 +653,73 @@ export function createModel (builder: Builder): void {
indexes: [] indexes: []
} }
) )
builder.mixin<Class<BrowserNotification>, IndexingConfiguration<BrowserNotification>>(
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 ( export function generateClassNotificationTypes (
@ -687,6 +735,8 @@ export function generateClassNotificationTypes (
hierarchy.isDerived(_class, core.class.AttachedDoc) ? core.class.AttachedDoc : core.class.Doc 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 filtered = Array.from(attributes.values()).filter((p) => p.hidden !== true && p.readonly !== true)
const enabledInboxTypes: Ref<BaseNotificationType>[] = []
for (const attribute of filtered) { for (const attribute of filtered) {
if (ignoreKeys.includes(attribute.name)) continue if (ignoreKeys.includes(attribute.name)) continue
const isCollection: boolean = core.class.Collection === attribute.type._class const isCollection: boolean = core.class.Collection === attribute.type._class
@ -704,9 +754,11 @@ export function generateClassNotificationTypes (
objectClass, objectClass,
txClasses, txClasses,
hidden: false, hidden: false,
providers: { defaultEnabled: false,
[notification.providers.PlatformNotification]: defaultEnabled.includes(attribute.name), templates: {
[notification.providers.BrowserNotification]: false textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{doc} updated'
}, },
label: attribute.label label: attribute.label
} }
@ -715,5 +767,17 @@ export function generateClassNotificationTypes (
} }
const id = `${notification.class.NotificationType}_${_class}_${attribute.name}` as Ref<NotificationType> const id = `${notification.class.NotificationType}_${_class}_${attribute.name}` as Ref<NotificationType>
builder.createDoc(notification.class.NotificationType, core.space.Model, data, id) 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
})
} }
} }

View File

@ -22,6 +22,7 @@ import {
type MigrationUpgradeClient type MigrationUpgradeClient
} from '@hcengineering/model' } from '@hcengineering/model'
import notification, { notificationId, type DocNotifyContext } from '@hcengineering/notification' import notification, { notificationId, type DocNotifyContext } from '@hcengineering/notification'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import { DOMAIN_NOTIFICATION } from './index' import { DOMAIN_NOTIFICATION } from './index'
@ -67,6 +68,32 @@ export async function removeNotifications (
} }
} }
export async function migrateSettings (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_PREFERENCE,
{
_class: 'notification:class:NotificationSetting' as Ref<Class<Doc>>,
attachedTo: 'notification:providers:BrowserNotification' as Ref<Doc>
},
{
_class: notification.class.NotificationTypeSetting,
attachedTo: notification.providers.PushNotificationProvider
}
)
await client.update(
DOMAIN_PREFERENCE,
{
_class: 'notification:class:NotificationSetting' as Ref<Class<Doc>>,
attachedTo: 'notification:providers:PlatformNotification' as Ref<Doc>
},
{
_class: notification.class.NotificationTypeSetting,
attachedTo: notification.providers.InboxNotificationProvider
}
)
}
export const notificationOperation: MigrateOperation = { export const notificationOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, notificationId, [ await tryMigrate(client, notificationId, [
@ -96,6 +123,10 @@ export const notificationOperation: MigrateOperation = {
DOMAIN_NOTIFICATION DOMAIN_NOTIFICATION
]) ])
} }
},
{
state: 'migrate-setting',
func: migrateSettings
} }
]) ])
}, },

View File

@ -118,7 +118,7 @@ export function createModel (builder: Builder): void {
}) })
builder.mixin(recruit.class.Applicant, core.class.Class, notification.mixin.ClassCollaborators, { 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, { builder.mixin(recruit.mixin.Candidate, core.class.Mixin, view.mixin.ObjectFactory, {
@ -1314,11 +1314,7 @@ export function createModel (builder: Builder): void {
htmlTemplate: '<p>{doc} was assigned to you by {sender}</p>', htmlTemplate: '<p>{doc} was assigned to you by {sender}</p>',
subjectTemplate: '{doc} was assigned to you' subjectTemplate: '{doc} was assigned to you'
}, },
providers: { defaultEnabled: true
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.EmailNotification]: true
}
}, },
recruit.ids.AssigneeNotification recruit.ids.AssigneeNotification
) )
@ -1354,9 +1350,11 @@ export function createModel (builder: Builder): void {
txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc],
objectClass: recruit.class.Applicant, objectClass: recruit.class.Applicant,
spaceSubscribe: true, spaceSubscribe: true,
providers: { defaultEnabled: false,
[notification.providers.PlatformNotification]: false, templates: {
[notification.providers.BrowserNotification]: false textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
} }
}, },
recruit.ids.ApplicationCreateNotification recruit.ids.ApplicationCreateNotification

View File

@ -135,8 +135,11 @@ export function createReviewModel (builder: Builder): void {
group: recruit.ids.ReviewNotificationGroup, group: recruit.ids.ReviewNotificationGroup,
txClasses: [core.class.TxCreateDoc], txClasses: [core.class.TxCreateDoc],
objectClass: recruit.class.Review, objectClass: recruit.class.Review,
providers: { defaultEnabled: true,
[notification.providers.PlatformNotification]: true templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
} }
}, },
recruit.ids.ReviewCreateNotification recruit.ids.ReviewCreateNotification

View File

@ -134,11 +134,7 @@ export function createModel (builder: Builder): void {
group: request.ids.RequestNotificationGroup, group: request.ids.RequestNotificationGroup,
label: request.string.Request, label: request.string.Request,
allowedForAuthor: true, allowedForAuthor: true,
providers: { defaultEnabled: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true,
[notification.providers.EmailNotification]: true
},
templates: { templates: {
textTemplate: '{sender} sent you a {doc}', textTemplate: '{sender} sent you a {doc}',
htmlTemplate: '<p><b>{sender}</b> sent you a {doc}</p>', htmlTemplate: '<p><b>{sender}</b> sent you a {doc}</p>',

View File

@ -17,11 +17,17 @@ import { type Builder } from '@hcengineering/model'
import serverCore from '@hcengineering/server-core' import serverCore from '@hcengineering/server-core'
import core from '@hcengineering/core/src/component' import core from '@hcengineering/core/src/component'
import serverActivity from '@hcengineering/server-activity' import serverActivity from '@hcengineering/server-activity'
import serverNotification from '@hcengineering/server-notification'
import activity from '@hcengineering/activity'
export { activityServerOperation } from './migration' export { activityServerOperation } from './migration'
export { serverActivityId } from '@hcengineering/server-activity' export { serverActivityId } from '@hcengineering/server-activity'
export function createModel (builder: Builder): void { 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, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverActivity.trigger.OnReactionChanged, trigger: serverActivity.trigger.OnReactionChanged,
txMatch: { txMatch: {

View File

@ -24,14 +24,18 @@ import serverChunter from '@hcengineering/server-chunter'
export { serverChunterId } from '@hcengineering/server-chunter' export { serverChunterId } from '@hcengineering/server-chunter'
export function createModel (builder: Builder): void { 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 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 presenter: serverChunter.function.ChannelTextPresenter
}) })
builder.mixin(chunter.class.ChatMessage, core.class.Class, serverNotification.mixin.TextPresenter, {
presenter: serverChunter.function.ChatMessageTextPresenter
})
builder.mixin<Class<Doc>, ObjectDDParticipant>( builder.mixin<Class<Doc>, ObjectDDParticipant>(
chunter.class.ChatMessage, chunter.class.ChatMessage,
core.class.Class, core.class.Class,

View File

@ -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, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverGmail.trigger.OnMessageCreate, trigger: serverGmail.trigger.OnMessageCreate,
txMatch: { txMatch: {

View File

@ -14,12 +14,12 @@
// limitations under the License. // 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 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 { 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 { type Resource } from '@hcengineering/platform'
import serverCore, { type TriggerControl } from '@hcengineering/server-core' import serverCore, { type TriggerControl } from '@hcengineering/server-core'
import serverNotification, { import serverNotification, {
@ -28,7 +28,9 @@ import serverNotification, {
type Presenter, type Presenter,
type TextPresenter, type TextPresenter,
type TypeMatch, type TypeMatch,
type NotificationContentProvider type NotificationContentProvider,
type NotificationProviderResources,
type NotificationProviderFunc
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
export { serverNotificationId } 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<NotificationProvider>
fn!: Resource<NotificationProviderFunc>
}
export function createModel (builder: Builder): void { 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, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.OnActivityNotificationViewed, trigger: serverNotification.trigger.OnActivityNotificationViewed,

View File

@ -566,10 +566,7 @@ export function createModel (builder: Builder): void {
htmlTemplate: '<p>Integration with {doc} was disabled</p>', htmlTemplate: '<p>Integration with {doc} was disabled</p>',
subjectTemplate: 'Integration with {doc} was disabled' subjectTemplate: 'Integration with {doc} was disabled'
}, },
providers: { defaultEnabled: true
[notification.providers.PlatformNotification]: true,
[notification.providers.EmailNotification]: true
}
}, },
setting.ids.IntegrationDisabledNotification setting.ids.IntegrationDisabledNotification
) )

View File

@ -199,13 +199,17 @@ export function createModel (builder: Builder): void {
txClasses: [core.class.TxCreateDoc], txClasses: [core.class.TxCreateDoc],
objectClass: telegram.class.Message, objectClass: telegram.class.Message,
group: telegram.ids.NotificationGroup, group: telegram.ids.NotificationGroup,
providers: { defaultEnabled: false
[notification.providers.PlatformNotification]: true
}
}, },
telegram.ids.NewMessageNotification 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, { builder.mixin(telegram.class.Message, core.class.Class, core.mixin.FullTextSearchContext, {
parentPropagate: false, parentPropagate: false,
childProcessingAllowed: true childProcessingAllowed: true

View File

@ -369,13 +369,22 @@ export function createModel (builder: Builder): void {
txClasses: [core.class.TxCreateDoc], txClasses: [core.class.TxCreateDoc],
objectClass: time.class.ProjectToDo, objectClass: time.class.ProjectToDo,
onlyOwn: true, onlyOwn: true,
providers: { defaultEnabled: false,
[notification.providers.PlatformNotification]: true templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
} }
}, },
time.ids.ToDoCreated 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, { builder.mixin(time.class.ToDo, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['user'] fields: ['user']
}) })

View File

@ -149,11 +149,7 @@ function defineNotifications (builder: Builder): void {
htmlTemplate: '<p>{doc} was assigned to you by {sender}</p>', htmlTemplate: '<p>{doc} was assigned to you by {sender}</p>',
subjectTemplate: '{doc} was assigned to you' subjectTemplate: '{doc} was assigned to you'
}, },
providers: { defaultEnabled: true
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.EmailNotification]: true
}
}, },
tracker.ids.AssigneeNotification tracker.ids.AssigneeNotification
) )

View File

@ -587,10 +587,7 @@ function defineTrainingRequest (builder: Builder): void {
group: training.notification.TrainingGroup, group: training.notification.TrainingGroup,
txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc],
objectClass: training.class.TrainingRequest, objectClass: training.class.TrainingRequest,
providers: { defaultEnabled: true,
[notification.providers.EmailNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: { templates: {
textTemplate: '{sender} sent you a training request {doc}', textTemplate: '{sender} sent you a training request {doc}',
htmlTemplate: '<p><b>{sender}</b> sent you a training request {doc}</p>', htmlTemplate: '<p><b>{sender}</b> sent you a training request {doc}</p>',

View File

@ -1,19 +1,26 @@
import { type Class, type Doc, type Ref } from '@hcengineering/core' 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 { getClient } from '.'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
const sounds = new Map<Asset, AudioBufferSourceNode>() const sounds = new Map<Asset, AudioBufferSourceNode>()
const context = new AudioContext() const context = new AudioContext()
export async function prepareSound (key: string, _class?: Ref<Class<Doc>>, loop = false, play = false): Promise<void> { export async function prepareSound (key: string, _class?: Ref<Class<Doc>>, loop = false, play = false): Promise<void> {
const notificationType = if (_class === undefined) return
_class !== undefined
? getClient().getModel().findAllSync(notification.class.NotificationType, { objectClass: _class }) const client = getClient()
: undefined const notificationType = client
const notAllowed = notificationType?.[0].providers[notification.providers.SoundNotification] === false .getModel()
if (notificationType === undefined || notAllowed) { .findAllSync(notification.class.NotificationType, { objectClass: _class })[0]
return
} if (notificationType === undefined) return
const isAllowedFn = await getResource(notification.function.IsNotificationAllowed)
const allowed: boolean = isAllowedFn(notificationType, notification.providers.SoundNotificationProvider)
if (!allowed) return
try { try {
const soundUrl = getMetadata(key as Asset) as string const soundUrl = getMetadata(key as Asset) as string
const audioBuffer = await fetch(soundUrl) const audioBuffer = await fetch(soundUrl)

View File

@ -702,6 +702,7 @@ input.search {
.w-32 { width: 8rem; } .w-32 { width: 8rem; }
.w-60 { width: 15rem; } .w-60 { width: 15rem; }
.w-85 { width: 21.25rem; } .w-85 { width: 21.25rem; }
.w-120 { width: 30rem; }
.w-165 { width: 41.25rem; } .w-165 { width: 41.25rem; }
.min-w-0 { min-width: 0; } .min-w-0 { min-width: 0; }
.min-w-2 { min-width: .5rem; } .min-w-2 { min-width: .5rem; }

View File

@ -43,6 +43,8 @@
"Thread": "Thread", "Thread": "Thread",
"AddReaction": "Add reaction", "AddReaction": "Add reaction",
"SaveForLater": "Save for later", "SaveForLater": "Save for later",
"RemoveFromLater": "Remove from saved" "RemoveFromLater": "Remove from saved",
"ReactionNotificationTitle": "Reaction on {title}",
"ReactionNotificationBody": "{senderName}: {reaction}"
} }
} }

View File

@ -42,6 +42,8 @@
"Thread": "Tópico", "Thread": "Tópico",
"AddReaction": "Adicionar Reacción", "AddReaction": "Adicionar Reacción",
"SaveForLater": "Guardar para mas tarde", "SaveForLater": "Guardar para mas tarde",
"RemoveFromLater": "Remover de los Guardados" "RemoveFromLater": "Remover de los Guardados",
"ReactionNotificationTitle": "Reacción sobre {title}",
"ReactionNotificationBody": "{senderName}: {reaction}"
} }
} }

View File

@ -43,6 +43,8 @@
"Thread": "Fil", "Thread": "Fil",
"AddReaction": "Ajouter une réaction", "AddReaction": "Ajouter une réaction",
"SaveForLater": "Enregistrer pour plus tard", "SaveForLater": "Enregistrer pour plus tard",
"RemoveFromLater": "Retirer des enregistrements" "RemoveFromLater": "Retirer des enregistrements",
"ReactionNotificationTitle": "Réaction sur {title}",
"ReactionNotificationBody": "{senderName}: {reaction}"
} }
} }

View File

@ -42,6 +42,8 @@
"Thread": "Tópico", "Thread": "Tópico",
"AddReaction": "Adicionar Reação", "AddReaction": "Adicionar Reação",
"SaveForLater": "Guardar para mais tarde", "SaveForLater": "Guardar para mais tarde",
"RemoveFromLater": "Remover dos Guardados" "RemoveFromLater": "Remover dos Guardados",
"ReactionNotificationTitle": "Reação em {title}",
"ReactionNotificationBody": "{senderName}: {reaction}"
} }
} }

View File

@ -43,6 +43,8 @@
"Thread": "Обсуждение", "Thread": "Обсуждение",
"AddReaction": "Добавить реакцию", "AddReaction": "Добавить реакцию",
"SaveForLater": "Cохранить", "SaveForLater": "Cохранить",
"RemoveFromLater": "Удалить из сохраненных" "RemoveFromLater": "Удалить из сохраненных",
"ReactionNotificationTitle": "Реакция на {title}",
"ReactionNotificationBody": "{senderName}: {reaction}"
} }
} }

View File

@ -43,6 +43,8 @@
"Thread": "线程", "Thread": "线程",
"AddReaction": "添加回应", "AddReaction": "添加回应",
"SaveForLater": "稍后保存", "SaveForLater": "稍后保存",
"RemoveFromLater": "从保存中移除" "RemoveFromLater": "从保存中移除",
"ReactionNotificationTitle": "反應於 {title}",
"ReactionNotificationBody": "{senderName}: {reaction}"
} }
} }

View File

@ -302,7 +302,9 @@ export default plugin(activityId, {
Mentions: '' as IntlString, Mentions: '' as IntlString,
MentionedYouIn: '' as IntlString, MentionedYouIn: '' as IntlString,
Messages: '' as IntlString, Messages: '' as IntlString,
Thread: '' as IntlString Thread: '' as IntlString,
ReactionNotificationTitle: '' as IntlString,
ReactionNotificationBody: '' as IntlString
}, },
component: { component: {
Activity: '' as AnyComponent, Activity: '' as AnyComponent,

View File

@ -28,6 +28,7 @@
import IconAttachments from './icons/Attachments.svelte' import IconAttachments from './icons/Attachments.svelte'
import UploadDuo from './icons/UploadDuo.svelte' import UploadDuo from './icons/UploadDuo.svelte'
export let object: Doc | undefined = undefined
export let objectId: Ref<Doc> export let objectId: Ref<Doc>
export let space: Ref<Space> export let space: Ref<Space>
export let _class: Ref<Class<Doc>> export let _class: Ref<Class<Doc>>
@ -57,7 +58,7 @@
await createAttachments( await createAttachments(
client, client,
list, list,
{ objectClass: _class, objectId, space }, { objectClass: object?._class ?? _class, objectId, space },
attachmentClass, attachmentClass,
attachmentClassOptions attachmentClassOptions
) )

View File

@ -37,6 +37,7 @@
"NewIncomingMessage": "Sent you a new email", "NewIncomingMessage": "Sent you a new email",
"ConfigLabel": "Email", "ConfigLabel": "Email",
"ConfigDescription": "Extension for Gmail email integration", "ConfigDescription": "Extension for Gmail email integration",
"GooglePrivacy": "Hulys use and transfer of information received from Google APIs to any other app will adhere to <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, including the Limited Use requirements." "GooglePrivacy": "Hulys use and transfer of information received from Google APIs to any other app will adhere to <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, including the Limited Use requirements.",
"EmailNotificationsDescription": "Receive personal notifications on email."
} }
} }

View File

@ -37,6 +37,7 @@
"NewIncomingMessage": "Te ha enviado un nuevo correo electrónico", "NewIncomingMessage": "Te ha enviado un nuevo correo electrónico",
"ConfigLabel": "Correo Electrónico", "ConfigLabel": "Correo Electrónico",
"ConfigDescription": "Extensión para la integración de correo electrónico de Gmail", "ConfigDescription": "Extensión para la integración de correo electrónico de Gmail",
"GooglePrivacy": "Hulys use and transfer of information received from Google APIs to any other app will adhere to <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, including the Limited Use requirements." "GooglePrivacy": "Hulys use and transfer of information received from Google APIs to any other app will adhere to <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, including the Limited Use requirements.",
"EmailNotificationsDescription": "Reciba notificaciones personales por correo electrónico."
} }
} }

View File

@ -37,6 +37,7 @@
"NewIncomingMessage": "Vous a envoyé un nouvel email", "NewIncomingMessage": "Vous a envoyé un nouvel email",
"ConfigLabel": "Email", "ConfigLabel": "Email",
"ConfigDescription": "Extension pour l'intégration des emails Gmail", "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 <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">règles d'utilisation des données utilisateur des services API Google</a>, 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 <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">règles d'utilisation des données utilisateur des services API Google</a>, y compris les exigences d'utilisation limitée.",
"EmailNotificationsDescription": "Recevez des notifications personnelles par e-mail."
} }
} }

View File

@ -37,6 +37,7 @@
"NewIncomingMessage": "Recebeu um novo email", "NewIncomingMessage": "Recebeu um novo email",
"ConfigLabel": "Email", "ConfigLabel": "Email",
"ConfigDescription": "Extensão para a integração de email do Gmail", "ConfigDescription": "Extensão para a integração de email do Gmail",
"GooglePrivacy": "Hulys use and transfer of information received from Google APIs to any other app will adhere to <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, including the Limited Use requirements." "GooglePrivacy": "Hulys use and transfer of information received from Google APIs to any other app will adhere to <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API Services User Data Policy</a>, including the Limited Use requirements.",
"EmailNotificationsDescription": "Receba notificações pessoais por e-mail."
} }
} }

View File

@ -37,6 +37,7 @@
"NewIncomingMessage": "Прислал вам новое сообщение", "NewIncomingMessage": "Прислал вам новое сообщение",
"ConfigLabel": "Электронная почта", "ConfigLabel": "Электронная почта",
"ConfigDescription": "Расширение по работе с Gmail электронной почтой", "ConfigDescription": "Расширение по работе с Gmail электронной почтой",
"GooglePrivacy": "Использование и передача информации, полученной Huly от Google API, будет соответствовать <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Политике использования данных пользователей Google API</a>, включая требования ограниченного использования." "GooglePrivacy": "Использование и передача информации, полученной Huly от Google API, будет соответствовать <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Политике использования данных пользователей Google API</a>, включая требования ограниченного использования.",
"EmailNotificationsDescription": "Получайте персональные уведомления на электронную почту."
} }
} }

View File

@ -37,6 +37,7 @@
"NewIncomingMessage": "给您发送了一封新邮件", "NewIncomingMessage": "给您发送了一封新邮件",
"ConfigLabel": "电子邮件", "ConfigLabel": "电子邮件",
"ConfigDescription": "Gmail 邮件集成扩展", "ConfigDescription": "Gmail 邮件集成扩展",
"GooglePrivacy": "Huly 从 Google API 接收的信息的使用和传输将遵守 <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API 服务用户数据政策</a>,包括有限使用要求。" "GooglePrivacy": "Huly 从 Google API 接收的信息的使用和传输将遵守 <a href=\"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes\" target=\"_blank\">Google API 服务用户数据政策</a>,包括有限使用要求。",
"EmailNotificationsDescription": "透過電子郵件接收個人通知。"
} }
} }

View File

@ -15,7 +15,7 @@
import { ChannelItem } from '@hcengineering/contact' import { ChannelItem } from '@hcengineering/contact'
import type { Account, AttachedDoc, Class, Doc, Ref, Timestamp } from '@hcengineering/core' 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 type { IntlString, Plugin } from '@hcengineering/platform'
import { Metadata, plugin } from '@hcengineering/platform' import { Metadata, plugin } from '@hcengineering/platform'
import type { Handler, IntegrationType } from '@hcengineering/setting' import type { Handler, IntegrationType } from '@hcengineering/setting'
@ -89,7 +89,8 @@ export default plugin(gmailId, {
}, },
string: { string: {
From: '' as IntlString, From: '' as IntlString,
To: '' as IntlString To: '' as IntlString,
EmailNotificationsDescription: '' as IntlString
}, },
integrationType: { integrationType: {
Gmail: '' as Ref<IntegrationType> Gmail: '' as Ref<IntegrationType>
@ -108,5 +109,8 @@ export default plugin(gmailId, {
}, },
metadata: { metadata: {
GmailURL: '' as Metadata<string> GmailURL: '' as Metadata<string>
},
providers: {
EmailNotificationProvider: '' as Ref<NotificationProvider>
} }
}) })

View File

@ -49,6 +49,12 @@
"Unreads": "Unreads", "Unreads": "Unreads",
"EnablePush": "Enable push notifications", "EnablePush": "Enable push notifications",
"NotificationBlockedInBrowser": "Notifications are blocked in your browser. Please enable notifications in your browser settings", "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."
} }
} }

View File

@ -48,6 +48,12 @@
"Push": "Push", "Push": "Push",
"EnablePush": "Habilitar notificaciones 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.", "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}"
} }
} }

View File

@ -49,6 +49,12 @@
"Unreads": "Non lus", "Unreads": "Non lus",
"EnablePush": "Activer les notifications push", "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", "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}"
} }
} }

View File

@ -48,6 +48,12 @@
"Push": "Push", "Push": "Push",
"EnablePush": "Ativar notificações push", "EnablePush": "Ativar notificações push",
"NotificationBlockedInBrowser": "Notificações bloqueadas no navegador. Por favor habilite las notificaciones en la configuración de su navegador.", "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}"
} }
} }

View File

@ -49,6 +49,12 @@
"Unreads": "Непрочитанные", "Unreads": "Непрочитанные",
"EnablePush": "Включить Push-уведомления", "EnablePush": "Включить Push-уведомления",
"NotificationBlockedInBrowser": "Уведомления заблокированы в вашем браузере. Пожалуйста, включите уведомления в настройках браузера", "NotificationBlockedInBrowser": "Уведомления заблокированы в вашем браузере. Пожалуйста, включите уведомления в настройках браузера",
"Sound": "Звук" "General": "Основное",
"InboxNotificationsDescription": "Получайте персональные уведомления на свой почтовый ящик Huly.",
"PushNotificationsDescription": "Получайте персональные уведомления на рабочий стол.",
"Sound": "Звук",
"SoundNotificationsDescription": "Получайте звуковые уведомления о событиях.",
"CommonNotificationCollectionAdded": "{senderName} добавил {collection}",
"CommonNotificationCollectionRemoved": "{senderName} удалил {collection}"
} }
} }

View File

@ -49,6 +49,12 @@
"Unreads": "未读", "Unreads": "未读",
"EnablePush": "启用推送通知", "EnablePush": "启用推送通知",
"NotificationBlockedInBrowser": "通知在您的浏览器中被阻止。请在浏览器设置中启用通知", "NotificationBlockedInBrowser": "通知在您的浏览器中被阻止。请在浏览器设置中启用通知",
"Sound": "声音" "General": "通用",
"InboxNotificationsDescription": "在您的 Huly 收件匣中接收個人通知。",
"PushNotificationsDescription": "在桌面上接收個人通知。",
"Sound": "声音",
"SoundNotificationsDescription": "接收事件的声音通知。",
"CommonNotificationCollectionAdded": "{senderName} 添加了 {collection}",
"CommonNotificationCollectionRemoved": "{senderName} 移除了 {collection}"
} }
} }

View File

@ -1,158 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import core, { IdMap, Ref, toIdMap } from '@hcengineering/core'
import type {
BaseNotificationType,
NotificationGroup,
NotificationProvider,
NotificationSetting,
NotificationType
} from '@hcengineering/notification'
import { IntlString, getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Grid, Label, ToggleWithLabel } from '@hcengineering/ui'
import notification from '../plugin'
export let group: Ref<NotificationGroup>
export let settings: Map<Ref<BaseNotificationType>, NotificationSetting[]>
const client = getClient()
$: types = client.getModel().findAllSync(notification.class.BaseNotificationType, { group })
$: typesMap = toIdMap(types)
const providers: NotificationProvider[] = client.getModel().findAllSync(notification.class.NotificationProvider, {})
const providersMap: IdMap<NotificationProvider> = toIdMap(providers)
$: column = providers.length + 1
function getStatus (
settings: Map<Ref<BaseNotificationType>, NotificationSetting[]>,
type: Ref<BaseNotificationType>,
provider: Ref<NotificationProvider>
): boolean {
const setting = getSetting(settings, type, provider)
if (setting !== undefined) return setting.enabled
const prov = providersMap.get(provider)
if (prov === undefined) return false
const typeValue = typesMap.get(type)
if (typeValue === undefined) return false
return typeValue?.providers?.[provider] ?? false
}
function changeHandler (type: Ref<BaseNotificationType>, provider: Ref<NotificationProvider>): (evt: any) => void {
return (evt: any) => {
void change(type, provider, evt.detail)
}
}
async function change (
typeId: Ref<BaseNotificationType>,
providerId: Ref<NotificationProvider>,
value: boolean
): Promise<void> {
const provider = providersMap.get(providerId)
if (provider === undefined) return
if (provider.onChange !== undefined) {
const f = await getResource(provider.onChange)
const res = await f(value)
if (!res) {
value = !value
}
}
const current = getSetting(settings, typeId, providerId)
if (current === undefined) {
await client.createDoc(notification.class.NotificationSetting, core.space.Workspace, {
attachedTo: providerId,
type: typeId,
enabled: value
})
} else {
await client.update(current, {
enabled: value
})
}
if (value) {
if (provider?.depends !== undefined) {
const current = getStatus(settings, typeId, provider.depends)
if (!current) {
await change(typeId, provider.depends, true)
}
}
} else {
const dependents = providers.filter((p) => p.depends === providerId)
for (const dependent of dependents) {
await change(typeId, dependent._id, false)
}
}
}
function getSetting (
map: Map<Ref<BaseNotificationType>, NotificationSetting[]>,
type: Ref<BaseNotificationType>,
provider: Ref<NotificationProvider>
): NotificationSetting | undefined {
const typeMap = map.get(type)
if (typeMap === undefined) return
return typeMap.find((p) => p.attachedTo === provider)
}
const isNotificationType = (type: BaseNotificationType): type is NotificationType => {
return type._class === notification.class.NotificationType
}
function getLabel (type: BaseNotificationType): IntlString {
if (isNotificationType(type) && type.attachedToClass !== undefined) {
return notification.string.AddedRemoved
}
return notification.string.Change
}
</script>
<div class="container">
<Grid {column} columnGap={5} rowGap={1.5}>
{#each types as type}
<div class="flex">
{#if type.generated}
<Label label={getLabel(type)} />:
{/if}
<Label label={type.label} />
</div>
{#each providers as provider (provider._id)}
{#if type.providers[provider._id] !== undefined}
<div class="toggle">
<ToggleWithLabel
label={provider.label}
on={getStatus(settings, type._id, provider._id)}
on:change={changeHandler(type._id, provider._id)}
/>
</div>
{:else}
<div />
{/if}
{/each}
{/each}
</Grid>
</div>
<style lang="scss">
.container {
width: fit-content;
}
.toggle {
width: fit-content;
}
</style>

View File

@ -0,0 +1,105 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import notification, { NotificationProvider } from '@hcengineering/notification'
import { Icon, Label, ModernToggle } from '@hcengineering/ui'
import core, { Ref } from '@hcengineering/core'
import { providersSettings } from '../../utils'
const client = getClient()
const providers = client
.getModel()
.findAllSync(notification.class.NotificationProvider, {})
.sort((provider1, provider2) => provider1.order - provider2.order)
function getProviderStatus (ref: Ref<NotificationProvider>): boolean {
const provider = providers.find(({ _id }) => _id === ref)
if (provider === undefined) return false
const setting = $providersSettings.find(({ attachedTo }) => attachedTo === provider._id)
return setting?.enabled ?? provider.defaultEnabled
}
async function updateStatus (ref: Ref<NotificationProvider>, enabled: boolean): Promise<void> {
const setting = $providersSettings.find(({ attachedTo }) => attachedTo === ref)
if (setting !== undefined) {
await client.update(setting, { enabled })
setting.enabled = enabled
} else {
await client.createDoc(notification.class.NotificationProviderSetting, core.space.Workspace, {
attachedTo: ref,
enabled
})
}
}
async function onToggle (provider: NotificationProvider): Promise<void> {
const setting = $providersSettings.find(({ attachedTo }) => attachedTo === provider._id)
const enabled = setting !== undefined ? !setting.enabled : !provider.defaultEnabled
await updateStatus(provider._id, enabled)
if (enabled && provider?.depends !== undefined) {
const current = getProviderStatus(provider.depends)
if (!current) {
await updateStatus(provider.depends, true)
}
} else if (!enabled) {
const dependents = providers.filter((p) => p.depends === provider._id)
for (const dependent of dependents) {
await updateStatus(dependent._id, false)
}
}
}
</script>
{#each providers as provider}
{@const setting = $providersSettings.find(({ attachedTo }) => attachedTo === provider._id)}
<div class="flex-row-center flex-gap-2">
<div class="flex-col flex-gap-1 mb-4 w-120">
<div class="flex-row-center flex-gap-2">
<Icon icon={provider.icon} size="medium" />
<span class="label font-semi-bold">
<Label label={provider.label} />
</span>
</div>
<span class="description">
<Label label={provider.description} />
</span>
</div>
{#if provider.canDisable}
<ModernToggle
size="small"
checked={setting?.enabled ?? provider.defaultEnabled}
on:change={() => onToggle(provider)}
/>
{/if}
</div>
{/each}
<style lang="scss">
.label {
color: var(--global-primary-TextColor);
}
.description {
color: var(--global-secondary-TextColor);
}
</style>

View File

@ -0,0 +1,194 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import core, { IdMap, Ref, toIdMap } from '@hcengineering/core'
import {
BaseNotificationType,
NotificationProvider,
NotificationGroup,
NotificationType,
NotificationTypeSetting,
NotificationProviderDefaults
} from '@hcengineering/notification'
import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Grid, Label, ModernToggle } from '@hcengineering/ui'
import notification from '../../plugin'
import { providersSettings } from '../../utils'
export let group: Ref<NotificationGroup>
export let settings: Map<Ref<BaseNotificationType>, NotificationTypeSetting[]>
const client = getClient()
const providers: NotificationProvider[] = client
.getModel()
.findAllSync(notification.class.NotificationProvider, {})
.sort((provider1, provider2) => provider1.order - provider2.order)
const providerDefaults: NotificationProviderDefaults[] = client
.getModel()
.findAllSync(notification.class.NotificationProviderDefaults, {})
const providersMap: IdMap<NotificationProvider> = toIdMap(providers)
$: types = client.getModel().findAllSync(notification.class.BaseNotificationType, { group })
$: typesMap = toIdMap(types)
function getStatus (
settings: Map<Ref<BaseNotificationType>, NotificationTypeSetting[]>,
type: Ref<BaseNotificationType>,
provider: Ref<NotificationProvider>
): boolean {
const setting = getTypeSetting(settings, type, provider)
if (setting !== undefined) return setting.enabled
const prov = providersMap.get(provider)
if (prov === undefined) return false
const providerEnabled = providerDefaults.some((it) => it.provider === provider && it.enabledTypes.includes(type))
if (providerEnabled) return true
const typeValue = typesMap.get(type)
if (typeValue === undefined) return false
return typeValue.defaultEnabled
}
async function onToggle (
typeId: Ref<BaseNotificationType>,
providerId: Ref<NotificationProvider>,
value: boolean
): Promise<void> {
const provider = providersMap.get(providerId)
if (provider === undefined) return
const currentSetting = getTypeSetting(settings, typeId, providerId)
if (currentSetting === undefined) {
await client.createDoc(notification.class.NotificationTypeSetting, core.space.Workspace, {
attachedTo: providerId,
type: typeId,
enabled: value
})
} else {
await client.update(currentSetting, {
enabled: value
})
}
if (value && provider?.depends !== undefined) {
const current = getStatus(settings, typeId, provider.depends)
if (!current) {
await onToggle(typeId, provider.depends, true)
}
} else if (!value) {
const dependents = providers.filter(({ depends }) => depends === providerId)
for (const dependent of dependents) {
await onToggle(typeId, dependent._id, false)
}
}
}
function getTypeSetting (
map: Map<Ref<BaseNotificationType>, NotificationTypeSetting[]>,
type: Ref<BaseNotificationType>,
provider: Ref<NotificationProvider>
): NotificationTypeSetting | undefined {
const typeMap = map.get(type)
if (typeMap === undefined) return
return typeMap.find((p) => p.attachedTo === provider)
}
const isNotificationType = (type: BaseNotificationType): type is NotificationType => {
return type._class === notification.class.NotificationType
}
function getLabel (type: BaseNotificationType): IntlString {
if (isNotificationType(type) && type.attachedToClass !== undefined) {
return notification.string.AddedRemoved
}
return notification.string.Change
}
function isIgnored (type: Ref<BaseNotificationType>, provider: NotificationProvider): boolean {
const ignored = providerDefaults.some((it) => provider._id === it.provider && it.ignoredTypes.includes(type))
if (ignored) return true
if (provider.ignoreAll === true) {
return !providerDefaults.some(
(it) => provider._id === it.provider && it.excludeIgnore !== undefined && it.excludeIgnore.includes(type)
)
}
return false
}
$: filteredProviders = providers.filter((provider) => {
const providerSetting = $providersSettings.find((p) => p.attachedTo === provider._id)
if (providerSetting !== undefined && !providerSetting.enabled) {
return false
}
if (providerSetting === undefined && !provider.defaultEnabled) {
return false
}
if (provider.ignoreAll === true) {
const ignoreExcluded = providerDefaults
.map((it) => (provider._id === it.provider && it.excludeIgnore !== undefined ? it.excludeIgnore : []))
.flat()
return types.some((type) => ignoreExcluded.includes(type._id))
}
return true
})
$: column = filteredProviders.length + 1
</script>
<div class="container">
<Grid {column} columnGap={5} rowGap={1.5}>
{#each types as type}
<div class="flex">
{#if type.generated}
<Label label={getLabel(type)} />:
{/if}
<Label label={type.label} />
</div>
{#each filteredProviders as provider (provider._id)}
{#if !isIgnored(type._id, provider)}
{@const status = getStatus(settings, type._id, provider._id)}
<div class="toggle">
<ModernToggle
size="small"
label={provider.label}
checked={status}
on:change={() => onToggle(type._id, provider._id, !status)}
/>
</div>
{:else}
<div />
{/if}
{/each}
{/each}
</Grid>
</div>
<style lang="scss">
.container {
width: fit-content;
}
.toggle {
width: fit-content;
}
</style>

View File

@ -19,26 +19,28 @@
BaseNotificationType, BaseNotificationType,
NotificationGroup, NotificationGroup,
NotificationPreferencesGroup, NotificationPreferencesGroup,
NotificationSetting NotificationTypeSetting
} from '@hcengineering/notification' } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { import {
Location,
Scroller,
getCurrentResolvedLocation,
navigate,
resolvedLocationStore,
Header,
Breadcrumb, Breadcrumb,
defineSeparators, defineSeparators,
settingsSeparators, getCurrentResolvedLocation,
Separator, Header,
Loading,
Location,
navigate,
NavItem, NavItem,
Loading resolvedLocationStore,
Scroller,
Separator,
settingsSeparators
} from '@hcengineering/ui' } from '@hcengineering/ui'
import notification from '../plugin'
import notification from '../../plugin'
import NotificationGroupSetting from './NotificationGroupSetting.svelte' import NotificationGroupSetting from './NotificationGroupSetting.svelte'
import { providersSettings, typesSettings } from '../../utils'
const client = getClient() const client = getClient()
const groups: NotificationGroup[] = client.getModel().findAllSync(notification.class.NotificationGroup, {}) const groups: NotificationGroup[] = client.getModel().findAllSync(notification.class.NotificationGroup, {})
@ -46,13 +48,14 @@
.getModel() .getModel()
.findAllSync(notification.class.NotificationPreferencesGroup, {}) .findAllSync(notification.class.NotificationPreferencesGroup, {})
let settings = new Map<Ref<BaseNotificationType>, NotificationSetting[]>() let settings = new Map<Ref<BaseNotificationType>, 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() settings = new Map()
for (const value of res) { for (const value of res) {
const arr = settings.get(value.type) ?? [] const arr = settings.get(value.type) ?? []
@ -60,7 +63,11 @@
settings.set(value.type, arr) settings.set(value.type, arr)
} }
settings = settings settings = settings
loading = false isTypeSettingLoading = false
})
const unsubscribeProviderSetting = providersSettings.subscribe(() => {
isProviderSettingLoading = false
}) })
let group: Ref<NotificationGroup> | undefined = undefined let group: Ref<NotificationGroup> | undefined = undefined
@ -74,14 +81,18 @@
} }
} }
onDestroy( const unsubscribeLocation = resolvedLocationStore.subscribe((loc) => {
resolvedLocationStore.subscribe((loc) => { void (async (loc: Location): Promise<void> => {
void (async (loc: Location): Promise<void> => { group = loc.path[4] as Ref<NotificationGroup>
group = loc.path[4] as Ref<NotificationGroup> currentPreferenceGroup = undefined
currentPreferenceGroup = undefined })(loc)
})(loc) })
})
) onDestroy(() => {
unsubscribeLocation()
unsubscribeTypeSetting()
unsubscribeProviderSetting()
})
defineSeparators('notificationSettings', settingsSeparators) defineSeparators('notificationSettings', settingsSeparators)
</script> </script>
@ -132,7 +143,7 @@
<div class="antiNav-space" /> <div class="antiNav-space" />
</Scroller> </Scroller>
</div> </div>
<Separator name={'notificationSettings'} index={0} color={'var(--theme-divider-color)'} /> <Separator name="notificationSettings" index={0} color={'var(--theme-divider-color)'} />
<div class="hulyComponent-content__column content"> <div class="hulyComponent-content__column content">
<Scroller align={'center'} padding={'var(--spacing-3)'} bottomPadding={'var(--spacing-3)'}> <Scroller align={'center'} padding={'var(--spacing-3)'} bottomPadding={'var(--spacing-3)'}>
<div class="hulyComponent-content"> <div class="hulyComponent-content">

View File

@ -17,7 +17,7 @@
import { type Resources } from '@hcengineering/platform' import { type Resources } from '@hcengineering/platform'
import Inbox from './components/inbox/Inbox.svelte' 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 NotificationPresenter from './components/NotificationPresenter.svelte'
import DocNotifyContextPresenter from './components/DocNotifyContextPresenter.svelte' import DocNotifyContextPresenter from './components/DocNotifyContextPresenter.svelte'
import CollaboratorsChanged from './components/activity/CollaboratorsChanged.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 CommonInboxNotificationPresenter from './components/inbox/CommonInboxNotificationPresenter.svelte'
import NotificationCollaboratorsChanged from './components/NotificationCollaboratorsChanged.svelte' import NotificationCollaboratorsChanged from './components/NotificationCollaboratorsChanged.svelte'
import ReactionNotificationPresenter from './components/ReactionNotificationPresenter.svelte' import ReactionNotificationPresenter from './components/ReactionNotificationPresenter.svelte'
import GeneralPreferencesGroup from './components/settings/GeneralPreferencesGroup.svelte'
import { import {
unsubscribe, unsubscribe,
resolveLocation, resolveLocation,
@ -42,7 +43,8 @@ import {
readAll, readAll,
unreadAll, unreadAll,
checkPermission, checkPermission,
unarchiveContextNotifications unarchiveContextNotifications,
isNotificationAllowed
} from './utils' } from './utils'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient' import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
@ -63,7 +65,8 @@ export default async (): Promise<Resources> => ({
ActivityInboxNotificationPresenter, ActivityInboxNotificationPresenter,
CommonInboxNotificationPresenter, CommonInboxNotificationPresenter,
NotificationCollaboratorsChanged, NotificationCollaboratorsChanged,
ReactionNotificationPresenter ReactionNotificationPresenter,
GeneralPreferencesGroup
}, },
function: { function: {
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
@ -73,7 +76,8 @@ export default async (): Promise<Resources> => ({
CanReadNotifyContext: canReadNotifyContext, CanReadNotifyContext: canReadNotifyContext,
CanUnReadNotifyContext: canUnReadNotifyContext, CanUnReadNotifyContext: canUnReadNotifyContext,
HasInboxNotifications: hasInboxNotifications, HasInboxNotifications: hasInboxNotifications,
CheckPushPermission: checkPermission CheckPushPermission: checkPermission,
IsNotificationAllowed: isNotificationAllowed
}, },
actionImpl: { actionImpl: {
Unsubscribe: unsubscribe, Unsubscribe: unsubscribe,

View File

@ -43,9 +43,13 @@ import notification, {
type DisplayInboxNotification, type DisplayInboxNotification,
type DocNotifyContext, type DocNotifyContext,
type InboxNotification, type InboxNotification,
type MentionInboxNotification type MentionInboxNotification,
type BaseNotificationType,
type NotificationProvider,
type NotificationProviderSetting,
type NotificationTypeSetting
} from '@hcengineering/notification' } from '@hcengineering/notification'
import { MessageBox, getClient } from '@hcengineering/presentation' import { MessageBox, getClient, createQuery } from '@hcengineering/presentation'
import { import {
getCurrentLocation, getCurrentLocation,
getLocation, getLocation,
@ -65,6 +69,23 @@ import { getObjectLinkId } from '@hcengineering/view-resources'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient' import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
import { type InboxData, type InboxNotificationsFilter } from './types' import { type InboxData, type InboxNotificationsFilter } from './types'
export const providersSettings = writable<NotificationProviderSetting[]>([])
export const typesSettings = writable<NotificationTypeSetting[]>([])
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<boolean> { export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
if (docNotifyContext.hidden) { if (docNotifyContext.hidden) {
return false return false
@ -777,3 +798,39 @@ export function notificationsComparator (notifications1: InboxNotification, noti
return 0 return 0
} }
export function isNotificationAllowed (type: BaseNotificationType, providerId: Ref<NotificationProvider>): 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
}

View File

@ -124,8 +124,7 @@ export interface BaseNotificationType extends Doc {
// allowed to change setting (probably we should show it, but disable toggle??) // allowed to change setting (probably we should show it, but disable toggle??)
hidden: boolean hidden: boolean
group: Ref<NotificationGroup> group: Ref<NotificationGroup>
// allowed providers and default value for it defaultEnabled: boolean
providers: Record<Ref<NotificationProvider>, boolean>
// templates for email (and browser/push?) // templates for email (and browser/push?)
templates?: NotificationTemplate templates?: NotificationTemplate
} }
@ -152,19 +151,30 @@ export interface NotificationType extends BaseNotificationType {
export interface CommonNotificationType extends BaseNotificationType {} export interface CommonNotificationType extends BaseNotificationType {}
/**
* @public
*/
export interface NotificationProvider extends Doc { export interface NotificationProvider extends Doc {
label: IntlString label: IntlString
description: IntlString
icon: Asset
defaultEnabled: boolean
depends?: Ref<NotificationProvider> depends?: Ref<NotificationProvider>
onChange?: Resource<(value: boolean) => Promise<boolean>> canDisable: boolean
ignoreAll?: boolean
order: number
} }
/** export interface NotificationProviderDefaults extends Doc {
* @public provider: Ref<NotificationProvider>
*/ excludeIgnore?: Ref<BaseNotificationType>[]
export interface NotificationSetting extends Preference { ignoredTypes: Ref<BaseNotificationType>[]
enabledTypes: Ref<BaseNotificationType>[]
}
export interface NotificationProviderSetting extends Preference {
attachedTo: Ref<NotificationProvider>
enabled: boolean
}
export interface NotificationTypeSetting extends Preference {
attachedTo: Ref<NotificationProvider> attachedTo: Ref<NotificationProvider>
type: Ref<BaseNotificationType> type: Ref<BaseNotificationType>
enabled: boolean enabled: boolean
@ -329,8 +339,6 @@ const notification = plugin(notificationId, {
BaseNotificationType: '' as Ref<Class<BaseNotificationType>>, BaseNotificationType: '' as Ref<Class<BaseNotificationType>>,
NotificationType: '' as Ref<Class<NotificationType>>, NotificationType: '' as Ref<Class<NotificationType>>,
CommonNotificationType: '' as Ref<Class<CommonNotificationType>>, CommonNotificationType: '' as Ref<Class<CommonNotificationType>>,
NotificationProvider: '' as Ref<Class<NotificationProvider>>,
NotificationSetting: '' as Ref<Class<NotificationSetting>>,
NotificationGroup: '' as Ref<Class<NotificationGroup>>, NotificationGroup: '' as Ref<Class<NotificationGroup>>,
NotificationPreferencesGroup: '' as Ref<Class<NotificationPreferencesGroup>>, NotificationPreferencesGroup: '' as Ref<Class<NotificationPreferencesGroup>>,
DocNotifyContext: '' as Ref<Class<DocNotifyContext>>, DocNotifyContext: '' as Ref<Class<DocNotifyContext>>,
@ -338,7 +346,11 @@ const notification = plugin(notificationId, {
ActivityInboxNotification: '' as Ref<Class<ActivityInboxNotification>>, ActivityInboxNotification: '' as Ref<Class<ActivityInboxNotification>>,
CommonInboxNotification: '' as Ref<Class<CommonInboxNotification>>, CommonInboxNotification: '' as Ref<Class<CommonInboxNotification>>,
ActivityNotificationViewlet: '' as Ref<Class<ActivityNotificationViewlet>>, ActivityNotificationViewlet: '' as Ref<Class<ActivityNotificationViewlet>>,
MentionInboxNotification: '' as Ref<Class<MentionInboxNotification>> MentionInboxNotification: '' as Ref<Class<MentionInboxNotification>>,
NotificationProvider: '' as Ref<Class<NotificationProvider>>,
NotificationTypeSetting: '' as Ref<Class<NotificationTypeSetting>>,
NotificationProviderSetting: '' as Ref<Class<NotificationProviderSetting>>,
NotificationProviderDefaults: '' as Ref<Mixin<NotificationProviderDefaults>>
}, },
ids: { ids: {
NotificationSettings: '' as Ref<Doc>, NotificationSettings: '' as Ref<Doc>,
@ -350,10 +362,9 @@ const notification = plugin(notificationId, {
PushPublicKey: '' as Metadata<string> PushPublicKey: '' as Metadata<string>
}, },
providers: { providers: {
PlatformNotification: '' as Ref<NotificationProvider>, InboxNotificationProvider: '' as Ref<NotificationProvider>,
BrowserNotification: '' as Ref<NotificationProvider>, PushNotificationProvider: '' as Ref<NotificationProvider>,
EmailNotification: '' as Ref<NotificationProvider>, SoundNotificationProvider: '' as Ref<NotificationProvider>
SoundNotification: '' as Ref<NotificationProvider>
}, },
integrationType: { integrationType: {
MobileApp: '' as Ref<IntegrationType> MobileApp: '' as Ref<IntegrationType>
@ -364,7 +375,8 @@ const notification = plugin(notificationId, {
CollaboratorsChanged: '' as AnyComponent, CollaboratorsChanged: '' as AnyComponent,
DocNotifyContextPresenter: '' as AnyComponent, DocNotifyContextPresenter: '' as AnyComponent,
NotificationCollaboratorsChanged: '' as AnyComponent, NotificationCollaboratorsChanged: '' as AnyComponent,
ReactionNotificationPresenter: '' as AnyComponent ReactionNotificationPresenter: '' as AnyComponent,
GeneralPreferencesGroup: '' as AnyComponent
}, },
action: { action: {
PinDocNotifyContext: '' as Ref<Action>, PinDocNotifyContext: '' as Ref<Action>,
@ -402,6 +414,12 @@ const notification = plugin(notificationId, {
YouAddedCollaborators: '' as IntlString, YouAddedCollaborators: '' as IntlString,
YouRemovedCollaborators: '' as IntlString, YouRemovedCollaborators: '' as IntlString,
Push: '' 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 Sound: '' as IntlString
}, },
function: { function: {
@ -410,6 +428,9 @@ const notification = plugin(notificationId, {
GetInboxNotificationsClient: '' as Resource<InboxNotificationsClientFactory>, GetInboxNotificationsClient: '' as Resource<InboxNotificationsClientFactory>,
HasInboxNotifications: '' as Resource< HasInboxNotifications: '' as Resource<
(notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean> (notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>
>,
IsNotificationAllowed: '' as Resource<
(type: BaseNotificationType, providerId: Ref<NotificationProvider>) => boolean
> >
}, },
resolver: { resolver: {

View File

@ -36,9 +36,12 @@ import type { TriggerControl } from '@hcengineering/server-core'
import { import {
createCollabDocInfo, createCollabDocInfo,
createCollaboratorNotifications, createCollaboratorNotifications,
getTextPresenter,
removeDocInboxNotifications removeDocInboxNotifications
} from '@hcengineering/server-notification-resources' } from '@hcengineering/server-notification-resources'
import { PersonAccount } from '@hcengineering/contact' import { PersonAccount } from '@hcengineering/contact'
import { NotificationContent } from '@hcengineering/notification'
import { getResource, translate } from '@hcengineering/platform'
import { getDocUpdateAction, getTxAttributesUpdates } from './utils' import { getDocUpdateAction, getTxAttributesUpdates } from './utils'
import { ReferenceTrigger } from './references' import { ReferenceTrigger } from './references'
@ -48,11 +51,16 @@ export async function OnReactionChanged (originTx: Tx, control: TriggerControl):
const innerTx = TxProcessor.extractTx(tx) as TxCUD<Reaction> const innerTx = TxProcessor.extractTx(tx) as TxCUD<Reaction>
if (innerTx._class === core.class.TxCreateDoc) { 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) { 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 [] return []
@ -411,6 +419,36 @@ async function OnDocRemoved (originTx: TxCUD<Doc>, control: TriggerControl): Pro
return messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id)) return messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id))
} }
async function ReactionNotificationContentProvider (
doc: ActivityMessage,
originTx: TxCUD<Doc>,
_: Ref<Account>,
control: TriggerControl
): Promise<NotificationContent> {
const tx = TxProcessor.extractTx(originTx) as TxCreateDoc<Reaction>
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' export * from './references'
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@ -420,5 +458,8 @@ export default async () => ({
ActivityMessagesHandler, ActivityMessagesHandler,
OnDocRemoved, OnDocRemoved,
OnReactionChanged OnReactionChanged
},
function: {
ReactionNotificationContentProvider
} }
}) })

View File

@ -21,6 +21,7 @@ import core, {
CollaborativeDoc, CollaborativeDoc,
Data, Data,
Doc, Doc,
generateId,
Hierarchy, Hierarchy,
Ref, Ref,
Space, Space,
@ -35,7 +36,7 @@ import core, {
TxUpdateDoc, TxUpdateDoc,
Type Type
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { MentionInboxNotification } from '@hcengineering/notification' import notification, { CommonInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
import { import {
extractReferences, extractReferences,
markupToPmNode, markupToPmNode,
@ -52,7 +53,8 @@ import {
shouldNotifyCommon, shouldNotifyCommon,
isShouldNotifyTx, isShouldNotifyTx,
NotifyResult, NotifyResult,
createPushFromInbox applyNotificationProviders,
getNotificationContent
} from '@hcengineering/server-notification-resources' } from '@hcengineering/server-notification-resources'
async function getPersonAccount (person: Ref<Person>, control: TriggerControl): Promise<PersonAccount | undefined> { async function getPersonAccount (person: Ref<Person>, control: TriggerControl): Promise<PersonAccount | undefined> {
@ -182,57 +184,61 @@ export async function getPersonNotificationTxes (
const notifyResult = await shouldNotifyCommon(control, receiver._id, notification.ids.MentionCommonNotificationType) const notifyResult = await shouldNotifyCommon(control, receiver._id, notification.ids.MentionCommonNotificationType)
const messageNotifyResult = await getMessageNotifyResult(reference, receiver, control, originTx, doc) const messageNotifyResult = await getMessageNotifyResult(reference, receiver, control, originTx, doc)
if (messageNotifyResult.allowed) { for (const [provider] of messageNotifyResult.entries()) {
notifyResult.allowed = false if (notifyResult.has(provider)) {
} notifyResult.delete(provider)
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<ActivityMessage>, 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)
}
} }
} }
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 return res
} }
@ -322,17 +328,17 @@ async function getMessageNotifyResult (
reference.attachedDocId === undefined || reference.attachedDocId === undefined ||
tx._class !== core.class.TxCreateDoc tx._class !== core.class.TxCreateDoc
) { ) {
return { allowed: false, emails: [], push: false } return new Map()
} }
const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators) const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators)
if (mixin === undefined || !mixin.collaborators.includes(account._id)) { 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)) { 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) return await isShouldNotifyTx(control, tx, originTx, doc, account, false, false, undefined)

View File

@ -15,6 +15,7 @@
import { Plugin, Resource, plugin } from '@hcengineering/platform' import { Plugin, Resource, plugin } from '@hcengineering/platform'
import type { TriggerFunc } from '@hcengineering/server-core' import type { TriggerFunc } from '@hcengineering/server-core'
import { NotificationContentProvider } from '@hcengineering/server-notification'
export * from './types' export * from './types'
export * from './utils' export * from './utils'
@ -33,5 +34,8 @@ export default plugin(serverActivityId, {
OnDocRemoved: '' as Resource<TriggerFunc>, OnDocRemoved: '' as Resource<TriggerFunc>,
OnReactionChanged: '' as Resource<TriggerFunc>, OnReactionChanged: '' as Resource<TriggerFunc>,
ReferenceTrigger: '' as Resource<TriggerFunc> ReferenceTrigger: '' as Resource<TriggerFunc>
},
function: {
ReactionNotificationContentProvider: '' as Resource<NotificationContentProvider>
} }
}) })

View File

@ -37,17 +37,18 @@ import core, {
TxUpdateDoc TxUpdateDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { Collaborators, NotificationContent } from '@hcengineering/notification' 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 serverCore, { TriggerControl } from '@hcengineering/server-core'
import { import {
createCollaboratorNotifications, createCollaboratorNotifications,
getDocCollaborators, getDocCollaborators,
getMixinTx getMixinTx
} from '@hcengineering/server-notification-resources' } from '@hcengineering/server-notification-resources'
import { stripTags } from '@hcengineering/text' import { markupToText, stripTags } from '@hcengineering/text'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
import { encodeObjectURI } from '@hcengineering/view'
/** /**
* @public * @public
@ -55,9 +56,10 @@ import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
export async function channelHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> { export async function channelHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const channel = doc as ChunterSpace const channel = doc as ChunterSpace
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? '' 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) const link = concatLink(front, path)
return `<a href='${link}'>${channel.name}</a>` const name = await channelTextPresenter(channel)
return `<a href='${link}'>${name}</a>`
} }
/** /**
@ -65,9 +67,18 @@ export async function channelHTMLPresenter (doc: Doc, control: TriggerControl):
*/ */
export async function channelTextPresenter (doc: Doc): Promise<string> { export async function channelTextPresenter (doc: Doc): Promise<string> {
const channel = doc as ChunterSpace const channel = doc as ChunterSpace
if (channel._class === chunter.class.DirectMessage) {
return await translate(chunter.string.Direct, {})
}
return `${channel.name}` return `${channel.name}`
} }
export async function ChatMessageTextPresenter (doc: ChatMessage): Promise<string> {
return markupToText(doc.message)
}
/** /**
* @public * @public
*/ */
@ -456,6 +467,7 @@ export default async () => ({
CommentRemove, CommentRemove,
ChannelHTMLPresenter: channelHTMLPresenter, ChannelHTMLPresenter: channelHTMLPresenter,
ChannelTextPresenter: channelTextPresenter, ChannelTextPresenter: channelTextPresenter,
ChunterNotificationContentProvider: getChunterNotificationContent ChunterNotificationContentProvider: getChunterNotificationContent,
ChatMessageTextPresenter
} }
}) })

View File

@ -37,6 +37,7 @@ export default plugin(serverChunterId, {
CommentRemove: '' as Resource<ObjectDDParticipantFunc>, CommentRemove: '' as Resource<ObjectDDParticipantFunc>,
ChannelHTMLPresenter: '' as Resource<Presenter>, ChannelHTMLPresenter: '' as Resource<Presenter>,
ChannelTextPresenter: '' as Resource<Presenter>, ChannelTextPresenter: '' as Resource<Presenter>,
ChunterNotificationContentProvider: '' as Resource<NotificationContentProvider> ChunterNotificationContentProvider: '' as Resource<NotificationContentProvider>,
ChatMessageTextPresenter: '' as Resource<Presenter>
} }
}) })

View File

@ -40,6 +40,8 @@
"@hcengineering/core": "^0.6.32", "@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/server-core": "^0.6.1", "@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/notification": "^0.6.23",
"@hcengineering/contact": "^0.6.24", "@hcengineering/contact": "^0.6.24",
"@hcengineering/gmail": "^0.6.22" "@hcengineering/gmail": "^0.6.22"

View File

@ -13,10 +13,11 @@
// limitations under the License. // limitations under the License.
// //
import contact, { Channel } from '@hcengineering/contact' import contact, { Channel, formatName } from '@hcengineering/contact'
import { import {
Account, Account,
Class, Class,
concatLink,
Doc, Doc,
DocumentQuery, DocumentQuery,
FindOptions, FindOptions,
@ -29,7 +30,10 @@ import {
} from '@hcengineering/core' } from '@hcengineering/core'
import gmail, { Message } from '@hcengineering/gmail' import gmail, { Message } from '@hcengineering/gmail'
import { TriggerControl } from '@hcengineering/server-core' 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 * @public
@ -131,6 +135,94 @@ export async function IsIncomingMessage (
return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn) return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn)
} }
export async function sendEmailNotification (
text: string,
html: string,
subject: string,
receiver: string
): Promise<void> {
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<BaseNotificationType>,
doc: Doc | undefined,
sender: UserInfo,
receiver: UserInfo,
data: InboxNotification
): Promise<void> {
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<Tx[]> => {
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 // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({ export default async () => ({
trigger: { trigger: {
@ -138,6 +230,7 @@ export default async () => ({
}, },
function: { function: {
IsIncomingMessage, IsIncomingMessage,
FindMessages FindMessages,
SendEmailNotifications
} }
}) })

View File

@ -17,7 +17,7 @@
import type { Plugin, Resource } from '@hcengineering/platform' import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import { ObjectDDParticipantFunc, TriggerFunc } from '@hcengineering/server-core' import { ObjectDDParticipantFunc, TriggerFunc } from '@hcengineering/server-core'
import { TypeMatchFunc } from '@hcengineering/server-notification' import { NotificationProviderFunc, TypeMatchFunc } from '@hcengineering/server-notification'
/** /**
* @public * @public
@ -33,6 +33,7 @@ export default plugin(serverGmailId, {
}, },
function: { function: {
IsIncomingMessage: '' as TypeMatchFunc, IsIncomingMessage: '' as TypeMatchFunc,
FindMessages: '' as Resource<ObjectDDParticipantFunc> FindMessages: '' as Resource<ObjectDDParticipantFunc>,
SendEmailNotifications: '' as Resource<NotificationProviderFunc>
} }
}) })

View File

@ -43,6 +43,8 @@
"@hcengineering/contact": "^0.6.24", "@hcengineering/contact": "^0.6.24",
"@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0", "@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/notification": "^0.6.23",
"@hcengineering/hr": "^0.6.19" "@hcengineering/hr": "^0.6.19"
} }

View File

@ -40,7 +40,9 @@ import notification, { NotificationType } from '@hcengineering/notification'
import { translate } from '@hcengineering/platform' import { translate } from '@hcengineering/platform'
import { TriggerControl } from '@hcengineering/server-core' import { TriggerControl } from '@hcengineering/server-core'
import { getEmployee, getPersonAccountById } from '@hcengineering/server-notification' 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 ( async function getOldDepartment (
currentTx: TxMixin<Employee, Staff> | TxUpdateDoc<Employee>, currentTx: TxMixin<Employee, Staff> | TxUpdateDoc<Employee>,
@ -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 ( async function sendEmailNotifications (
control: TriggerControl, control: TriggerControl,
sender: PersonAccount, sender: PersonAccount,
doc: Request | PublicHoliday, doc: Request | PublicHoliday,
space: Ref<Department>, space: Ref<Department>,
type: Ref<NotificationType> typeId: Ref<NotificationType>
): Promise<void> { ): Promise<void> {
const contacts = new Set<Ref<Contact>>() const contacts = new Set<Ref<Contact>>()
const departments = await buildHierarchy(space, control) const departments = await buildHierarchy(space, control)
@ -268,8 +271,14 @@ async function sendEmailNotifications (
const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { const accounts = await control.modelDb.findAll(contact.class.PersonAccount, {
person: { $in: Array.from(contacts.values()) as Ref<Employee>[] } person: { $in: Array.from(contacts.values()) as Ref<Employee>[] }
}) })
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) { 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) { if (!allowed) {
contacts.delete(account.person) contacts.delete(account.person)
} }
@ -283,7 +292,7 @@ async function sendEmailNotifications (
const senderPerson = (await control.findAll(contact.class.Person, { _id: sender.person }))[0] const senderPerson = (await control.findAll(contact.class.Person, { _id: sender.person }))[0]
const senderName = senderPerson !== undefined ? formatName(senderPerson.name, control.branding?.lastNameFirst) : '' 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 if (content === undefined) return
for (const channel of channels) { for (const channel of channels) {

View File

@ -13,7 +13,7 @@
// limitations under the License. // 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, { import core, {
Account, Account,
Ref, Ref,
@ -25,10 +25,7 @@ import core, {
TxUpdateDoc, TxUpdateDoc,
UserStatus UserStatus
} from '@hcengineering/core' } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { translate } from '@hcengineering/platform'
import { TriggerControl } from '@hcengineering/server-core' import { TriggerControl } from '@hcengineering/server-core'
import { createPushNotification, isAllowed } from '@hcengineering/server-notification-resources'
import love, { import love, {
Invite, Invite,
JoinRequest, JoinRequest,
@ -39,7 +36,10 @@ import love, {
isOffice, isOffice,
loveId loveId
} from '@hcengineering/love' } from '@hcengineering/love'
import { createPushNotification, isAllowed } from '@hcengineering/server-notification-resources'
import notification from '@hcengineering/notification'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import { translate } from '@hcengineering/platform'
export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]> { export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) as TxMixin<Person, Employee> const actualTx = TxProcessor.extractTx(tx) as TxMixin<Person, Employee>
@ -259,17 +259,18 @@ export async function OnKnock (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = [] const res: Tx[] = []
const from = (await control.findAll(contact.class.Person, { _id: request.person }))[0] const from = (await control.findAll(contact.class.Person, { _id: request.person }))[0]
if (from === undefined) return [] 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) { for (const user of roomInfo.persons) {
const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: user }) const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: user })
if (userAcc === undefined) continue if (userAcc === undefined) continue
if ( if (await isAllowed(control, userAcc._id, type, provider)) {
await isAllowed(
control,
userAcc._id,
love.ids.KnockNotification,
notification.providers.BrowserNotification
)
) {
const path = [workbenchId, control.workspace.workspaceUrl, loveId] const path = [workbenchId, control.workspace.workspaceUrl, loveId]
const title = await translate(love.string.KnockingLabel, {}) const title = await translate(love.string.KnockingLabel, {})
const body = await translate(love.string.IsKnocking, { const body = await translate(love.string.IsKnocking, {
@ -295,9 +296,15 @@ export async function OnInvite (tx: Tx, control: TriggerControl): Promise<Tx[]>
const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: target._id }) const userAcc = await control.modelDb.findOne(contact.class.PersonAccount, { person: target._id })
if (userAcc === undefined) return [] if (userAcc === undefined) return []
const from = (await control.findAll(contact.class.Person, { _id: invite.from }))[0] const from = (await control.findAll(contact.class.Person, { _id: invite.from }))[0]
if ( const type = await control.modelDb.findOne(notification.class.NotificationType, {
await isAllowed(control, userAcc._id, love.ids.InviteNotification, notification.providers.BrowserNotification) _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 path = [workbenchId, control.workspace.workspaceUrl, loveId]
const title = await translate(love.string.InivitingLabel, {}) const title = await translate(love.string.InivitingLabel, {})
const body = const body =

View File

@ -18,8 +18,6 @@ import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/acti
import chunter, { ChatMessage } from '@hcengineering/chunter' import chunter, { ChatMessage } from '@hcengineering/chunter'
import contact, { import contact, {
type AvatarInfo, type AvatarInfo,
Employee,
formatName,
getAvatarProviderId, getAvatarProviderId,
getGravatarUrl, getGravatarUrl,
Person, Person,
@ -74,7 +72,9 @@ import serverCore from '@hcengineering/server-core'
import serverNotification, { import serverNotification, {
getPersonAccount, getPersonAccount,
getPersonAccountById, getPersonAccountById,
NOTIFICATION_BODY_SIZE NOTIFICATION_BODY_SIZE,
UserInfo,
NOTIFICATION_TITLE_SIZE
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
import serverView from '@hcengineering/server-view' import serverView from '@hcengineering/server-view'
import { stripTags } from '@hcengineering/text' import { stripTags } from '@hcengineering/text'
@ -82,7 +82,7 @@ import { encodeObjectURI } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import webpush, { WebPushError } from 'web-push' import webpush, { WebPushError } from 'web-push'
import { Content, NotifyParams, NotifyResult, UserInfo } from './types' import { Content, NotifyParams, NotifyResult } from './types'
import { import {
getHTMLPresenter, getHTMLPresenter,
getNotificationContent, getNotificationContent,
@ -128,45 +128,47 @@ export async function getCommonNotificationTxes (
notifyResult: NotifyResult, notifyResult: NotifyResult,
_class = notification.class.CommonInboxNotification _class = notification.class.CommonInboxNotification
): Promise<Tx[]> { ): Promise<Tx[]> {
if (notifyResult.size === 0 || !notifyResult.has(notification.providers.InboxNotificationProvider)) {
return []
}
const res: Tx[] = [] const res: Tx[] = []
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo })
if (notifyResult.allowed) { const notificationTx = await pushInboxNotifications(
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo }) control,
res,
receiver,
attachedTo,
attachedToClass,
space,
notifyContexts,
data,
_class,
modifiedOn
)
await pushInboxNotifications( if (notificationTx !== undefined) {
control, const notificationData = TxProcessor.createDoc2Doc(notificationTx)
res, await applyNotificationProviders(
receiver, notificationData,
notifyResult,
attachedTo, attachedTo,
attachedToClass, attachedToClass,
space, control,
notifyContexts, res,
data, doc,
_class, receiver,
modifiedOn, sender
sender,
notifyResult.push
) )
} }
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 return res
} }
async function getTextPart (doc: Doc, control: TriggerControl): Promise<string | undefined> { async function getTextPart (doc: Doc, control: TriggerControl): Promise<string | undefined> {
const TextPresenter = getTextPresenter(doc._class, control.hierarchy) const TextPresenter = getTextPresenter(doc._class, control.hierarchy)
console.log({ _class: doc._class, presenter: TextPresenter })
if (TextPresenter === undefined) return if (TextPresenter === undefined) return
return await ( return await (
await getResource(TextPresenter.presenter) await getResource(TextPresenter.presenter)
@ -178,33 +180,52 @@ async function getHtmlPart (doc: Doc, control: TriggerControl): Promise<string |
return HTMLPresenter != null ? await (await getResource(HTMLPresenter.presenter))(doc, control) : undefined return HTMLPresenter != null ? await (await getResource(HTMLPresenter.presenter))(doc, control) : undefined
} }
function fillTemplate (template: string, sender: string, doc: string, data: string): string { function fillTemplate (
template: string,
sender: string,
doc: string,
data: string,
params: Record<string, string> = {}
): string {
let res = replaceAll(template, '{sender}', sender) let res = replaceAll(template, '{sender}', sender)
res = replaceAll(res, '{doc}', doc) res = replaceAll(res, '{doc}', doc)
res = replaceAll(res, '{data}', data) res = replaceAll(res, '{data}', data)
for (const key in params) {
res = replaceAll(res, `{${key}}`, params[key])
}
return res return res
} }
/** /**
* @public * @public
*/ */
export async function getContent ( export async function getContentByTemplate (
doc: Doc | undefined, doc: Doc | undefined,
sender: string, sender: string,
type: Ref<BaseNotificationType>, type: Ref<BaseNotificationType>,
control: TriggerControl, control: TriggerControl,
data: string data: string,
notificationData?: InboxNotification
): Promise<Content | undefined> { ): Promise<Content | undefined> {
if (doc === undefined) return if (doc === undefined) return
const notificationType = control.modelDb.getObject(type) const notificationType = control.modelDb.getObject(type)
if (notificationType.templates === undefined) return
const textPart = await getTextPart(doc, control) const textPart = await getTextPart(doc, control)
if (textPart === undefined) return if (textPart === undefined) return
if (notificationType.templates === undefined) return const params =
const text = fillTemplate(notificationType.templates.textTemplate, sender, textPart, data) 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 htmlPart = await getHtmlPart(doc, control)
const html = fillTemplate(notificationType.templates.htmlTemplate, sender, htmlPart ?? textPart, data) const html = fillTemplate(notificationType.templates.htmlTemplate, sender, htmlPart ?? textPart, data, params)
const subject = fillTemplate(notificationType.templates.subjectTemplate, sender, textPart, data) const subject = fillTemplate(notificationType.templates.subjectTemplate, sender, textPart, data, params)
if (subject === '') return
return { return {
text, text,
html, html,
@ -212,59 +233,6 @@ export async function getContent (
} }
} }
async function notifyByEmail (
control: TriggerControl,
type: Ref<BaseNotificationType>,
doc: Doc | undefined,
sender: UserInfo,
receiver: UserInfo,
data: string = ''
): Promise<void> {
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<void> {
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<Ref<Account>[]> { async function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Promise<Ref<Account>[]> {
const hierarchy = control.hierarchy const hierarchy = control.hierarchy
if (attr.type._class === core.class.RefTo) { if (attr.type._class === core.class.RefTo) {
@ -371,11 +339,8 @@ export async function pushInboxNotifications (
data: Partial<Data<InboxNotification>>, data: Partial<Data<InboxNotification>>,
_class: Ref<Class<InboxNotification>>, _class: Ref<Class<InboxNotification>>,
modifiedOn: Timestamp, modifiedOn: Timestamp,
sender: UserInfo, shouldUpdateTimestamp = true
shouldPush: boolean, ): Promise<TxCreateDoc<InboxNotification> | undefined> {
shouldUpdateTimestamp = true,
cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
): Promise<void> {
const account = target.account const account = target.account
if (account === undefined) { if (account === undefined) {
@ -427,28 +392,14 @@ export async function pushInboxNotifications (
} }
const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData) const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData)
res.push(notificationTx) res.push(notificationTx)
if (shouldPush) {
// const now = Date.now() return notificationTx
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)
}
}
} }
} }
async function activityInboxNotificationToText (doc: Data<ActivityInboxNotification>): Promise<[string, string]> { async function activityInboxNotificationToText (
doc: Data<ActivityInboxNotification>
): Promise<{ title: string, body: string, [key: string]: string }> {
let title: string = '' let title: string = ''
let body: string = '' let body: string = ''
@ -466,10 +417,12 @@ async function activityInboxNotificationToText (doc: Data<ActivityInboxNotificat
body = await translate(doc.body, params) body = await translate(doc.body, params)
} }
return [title, body] return { ...params, title: title.substring(0, NOTIFICATION_TITLE_SIZE), body }
} }
async function commonInboxNotificationToText (doc: Data<CommonInboxNotification>): Promise<[string, string]> { async function commonInboxNotificationToText (
doc: Data<CommonInboxNotification>
): Promise<{ title: string, body: string, [key: string]: string }> {
let title: string = '' let title: string = ''
let body: string = '' let body: string = ''
@ -492,13 +445,13 @@ async function commonInboxNotificationToText (doc: Data<CommonInboxNotification>
if (doc.message != null) { if (doc.message != null) {
body = await translate(doc.message, params) body = await translate(doc.message, params)
} }
return [title, body] return { ...params, title, body }
} }
async function mentionInboxNotificationToText ( async function mentionInboxNotificationToText (
doc: Data<MentionInboxNotification>, doc: Data<MentionInboxNotification>,
control: TriggerControl 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] let obj = (await control.findAll(doc.mentionedInClass, { _id: doc.mentionedIn }, { limit: 1 }))[0]
if (obj !== undefined) { if (obj !== undefined) {
if (control.hierarchy.isDerived(obj._class, chunter.class.ChatMessage)) { if (control.hierarchy.isDerived(obj._class, chunter.class.ChatMessage)) {
@ -525,6 +478,22 @@ async function mentionInboxNotificationToText (
return await commonInboxNotificationToText(doc) return await commonInboxNotificationToText(doc)
} }
async function getTranslatedNotificationContent (
data: Data<InboxNotification>,
_class: Ref<Class<InboxNotification>>,
control: TriggerControl
): Promise<{ title: string, body: string, [key: string]: string }> {
if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) {
return await activityInboxNotificationToText(data as Data<ActivityInboxNotification>)
} else if (control.hierarchy.isDerived(_class, notification.class.MentionInboxNotification)) {
return await mentionInboxNotificationToText(data as Data<MentionInboxNotification>, control)
} else if (control.hierarchy.isDerived(_class, notification.class.CommonInboxNotification)) {
return await commonInboxNotificationToText(data as Data<CommonInboxNotification>)
}
return { title: '', body: '' }
}
export async function createPushFromInbox ( export async function createPushFromInbox (
control: TriggerControl, control: TriggerControl,
target: UserInfo, target: UserInfo,
@ -536,15 +505,8 @@ export async function createPushFromInbox (
_id: Ref<Doc>, _id: Ref<Doc>,
cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>() cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
): Promise<Tx | undefined> { ): Promise<Tx | undefined> {
let title: string = '' const { title, body } = await getTranslatedNotificationContent(data, _class, control)
let body: string = ''
if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) {
;[title, body] = await activityInboxNotificationToText(data as Data<ActivityInboxNotification>)
} else if (control.hierarchy.isDerived(_class, notification.class.MentionInboxNotification)) {
;[title, body] = await mentionInboxNotificationToText(data as Data<MentionInboxNotification>, control)
} else if (control.hierarchy.isDerived(_class, notification.class.CommonInboxNotification)) {
;[title, body] = await commonInboxNotificationToText(data as Data<CommonInboxNotification>)
}
if (title === '' || body === '') { if (title === '' || body === '') {
return return
} }
@ -637,7 +599,7 @@ async function sendPushToSubscription (
try { try {
await webpush.sendNotification(subscription, JSON.stringify(data)) await webpush.sendNotification(subscription, JSON.stringify(data))
} catch (err) { } 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')) { if (err instanceof WebPushError && err.body.includes('expired')) {
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id) const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
await control.apply([tx]) await control.apply([tx])
@ -656,39 +618,78 @@ export async function pushActivityInboxNotifications (
sender: UserInfo, sender: UserInfo,
object: Doc, object: Doc,
docNotifyContexts: DocNotifyContext[], docNotifyContexts: DocNotifyContext[],
activityMessages: ActivityMessage[], activityMessage: ActivityMessage,
shouldUpdateTimestamp: boolean, shouldUpdateTimestamp: boolean
shouldPush: boolean, ): Promise<TxCreateDoc<InboxNotification> | undefined> {
cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
): Promise<void> {
if (target.account === undefined) { if (target.account === undefined) {
return return
} }
for (const activityMessage of activityMessages) { const content = await getNotificationContent(originTx, target.account, sender, object, control)
const content = await getNotificationContent(originTx, target.account, sender, object, control) const data: Partial<Data<ActivityInboxNotification>> = {
const data: Partial<Data<ActivityInboxNotification>> = { ...content,
...content, attachedTo: activityMessage._id,
attachedTo: activityMessage._id, attachedToClass: activityMessage._class
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<Doc>,
attachedToClass: Ref<Class<Doc>>,
control: TriggerControl,
res: Tx[],
object: Doc,
receiver: UserInfo,
sender: UserInfo
): Promise<void> {
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( const resource = resources.find((it) => it.provider === provider)
control,
res, if (resource === undefined) continue
target,
activityMessage.attachedTo, const fn = await getResource(resource.fn)
activityMessage.attachedToClass,
activityMessage.space, const txes = await fn(control, types, object, data, receiver, sender)
docNotifyContexts, if (txes.length > 0) {
data, res.push(...txes)
notification.class.ActivityInboxNotification, }
activityMessage.modifiedOn,
sender,
shouldPush,
shouldUpdateTimestamp,
cache
)
} }
} }
@ -697,14 +698,13 @@ export async function getNotificationTxes (
object: Doc, object: Doc,
tx: TxCUD<Doc>, tx: TxCUD<Doc>,
originTx: TxCUD<Doc>, originTx: TxCUD<Doc>,
target: UserInfo, receiver: UserInfo,
sender: UserInfo, sender: UserInfo,
params: NotifyParams, params: NotifyParams,
docNotifyContexts: DocNotifyContext[], docNotifyContexts: DocNotifyContext[],
activityMessages: ActivityMessage[], activityMessages: ActivityMessage[]
cache: Map<Ref<Doc>, Doc>
): Promise<Tx[]> { ): Promise<Tx[]> {
if (target.account === undefined) { if (receiver.account === undefined) {
return [] return []
} }
@ -717,39 +717,39 @@ export async function getNotificationTxes (
tx, tx,
originTx, originTx,
object, object,
target.account, receiver.account,
params.isOwn, params.isOwn,
params.isSpace, params.isSpace,
docMessage docMessage
) )
if (notifyResult.allowed) { if (notifyResult.has(notification.providers.InboxNotificationProvider)) {
await pushActivityInboxNotifications( const notificationTx = await pushActivityInboxNotifications(
originTx, originTx,
control, control,
res, res,
target, receiver,
sender, sender,
object, object,
docNotifyContexts, docNotifyContexts,
[message], message,
params.shouldUpdateTimestamp, params.shouldUpdateTimestamp
notifyResult.push,
cache
) )
}
if (notifyResult.emails.length === 0) { if (notificationTx !== undefined) {
continue const notificationData = TxProcessor.createDoc2Doc(notificationTx)
}
if (target.person !== undefined && control.hierarchy.isDerived(target.person._class, contact.mixin.Employee)) { await applyNotificationProviders(
const emp = target.person as Employee notificationData,
notifyResult,
if (emp?.active) { message.attachedTo,
for (const type of notifyResult.emails) { message.attachedToClass,
await notifyByEmail(control, type._id, object, sender, target) control,
} res,
object,
receiver,
sender
)
} }
} }
} }
@ -768,12 +768,11 @@ export async function createCollabDocInfo (
): Promise<Tx[]> { ): Promise<Tx[]> {
let res: Tx[] = [] let res: Tx[] = []
if (originTx.space === core.space.DerivedTx || collaborators.length === 0) { if (originTx.space === core.space.DerivedTx) {
return res return res
} }
const docMessages = activityMessages.filter((message) => message.attachedTo === object._id) const docMessages = activityMessages.filter((message) => message.attachedTo === object._id)
if (docMessages.length === 0) { if (docMessages.length === 0) {
return res return res
} }
@ -788,6 +787,10 @@ export async function createCollabDocInfo (
} }
} }
if (targets.size === 0) {
return res
}
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { const notifyContexts = await control.findAll(notification.class.DocNotifyContext, {
attachedTo: object._id attachedTo: object._id
}) })
@ -803,7 +806,7 @@ export async function createCollabDocInfo (
if (info === undefined) continue if (info === undefined) continue
res = res.concat( 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 return res
@ -908,8 +911,7 @@ async function updateCollaboratorsMixin (
tx: TxMixin<Doc, Collaborators>, tx: TxMixin<Doc, Collaborators>,
control: TriggerControl, control: TriggerControl,
activityMessages: ActivityMessage[], activityMessages: ActivityMessage[],
originTx: TxCUD<Doc>, originTx: TxCUD<Doc>
cache: Map<Ref<Doc>, Doc>
): Promise<Tx[]> { ): Promise<Tx[]> {
const { hierarchy } = control const { hierarchy } = control
@ -948,17 +950,23 @@ async function updateCollaboratorsMixin (
prevCollabs = mixin !== undefined ? new Set(await getDocCollaborators(prevDoc, mixin, control)) : new Set() 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) { for (const collab of tx.attributes.collaborators) {
if (!prevCollabs.has(collab) && tx.modifiedBy !== collab) { if (!prevCollabs.has(collab) && tx.modifiedBy !== collab) {
if ( for (const provider of providers) {
await isAllowed( if (await isAllowed(control, collab as Ref<PersonAccount>, type, provider)) {
control, newCollabs.push(collab)
collab as Ref<PersonAccount>, break
notification.ids.CollaboratoAddNotification, }
notification.providers.PlatformNotification
)
) {
newCollabs.push(collab)
} }
} }
} }
@ -977,19 +985,19 @@ async function updateCollaboratorsMixin (
if (target === undefined) continue if (target === undefined) continue
await pushActivityInboxNotifications( for (const message of activityMessages) {
originTx, await pushActivityInboxNotifications(
control, originTx,
res, control,
target, res,
sender, target,
prevDoc, sender,
docNotifyContexts, prevDoc,
activityMessages, docNotifyContexts,
true, message,
false, true
cache )
) }
} }
} }
} }
@ -1220,8 +1228,11 @@ export async function OnAttributeCreate (tx: Tx, control: TriggerControl): Promi
objectClass, objectClass,
txClasses, txClasses,
hidden: false, hidden: false,
providers: { defaultEnabled: false,
[notification.providers.PlatformNotification]: false templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{doc} updated'
}, },
label: attribute.label label: attribute.label
} }
@ -1330,13 +1341,7 @@ export async function createCollaboratorNotifications (
case core.class.TxMixin: { case core.class.TxMixin: {
let res = await updateCollaboratorDoc(tx as TxUpdateDoc<Doc>, control, originTx ?? tx, activityMessages, cache) let res = await updateCollaboratorDoc(tx as TxUpdateDoc<Doc>, control, originTx ?? tx, activityMessages, cache)
res = res.concat( res = res.concat(
await updateCollaboratorsMixin( await updateCollaboratorsMixin(tx as TxMixin<Doc, Collaborators>, control, activityMessages, originTx ?? tx)
tx as TxMixin<Doc, Collaborators>,
control,
activityMessages,
originTx ?? tx,
cache
)
) )
return await applyUserTxes(control, res) return await applyUserTxes(control, res)
} }

View File

@ -12,9 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { BaseNotificationType } from '@hcengineering/notification' import { BaseNotificationType, NotificationProvider } from '@hcengineering/notification'
import { Person, PersonAccount } from '@hcengineering/contact' import { Ref } from '@hcengineering/core'
import { Account, Ref } from '@hcengineering/core'
/** /**
* @public * @public
@ -28,20 +27,10 @@ export interface Content {
/** /**
* @public * @public
*/ */
export interface NotifyResult { export type NotifyResult = Map<Ref<NotificationProvider>, BaseNotificationType[]>
allowed: boolean
push: boolean
emails: BaseNotificationType[]
}
export interface NotifyParams { export interface NotifyParams {
isOwn: boolean isOwn: boolean
isSpace: boolean isSpace: boolean
shouldUpdateTimestamp: boolean shouldUpdateTimestamp: boolean
} }
export interface UserInfo {
_id: Ref<Account>
account?: PersonAccount
person?: Person
}

View File

@ -30,23 +30,26 @@ import core, {
MixinUpdate, MixinUpdate,
Ref, Ref,
Tx, Tx,
TxCreateDoc,
TxCUD, TxCUD,
TxMixin, TxMixin,
TxProcessor, TxProcessor,
TxRemoveDoc,
TxUpdateDoc TxUpdateDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import serverNotification, { import serverNotification, {
getPersonAccountById, getPersonAccountById,
HTMLPresenter, HTMLPresenter,
NotificationPresenter, NotificationPresenter,
TextPresenter TextPresenter,
UserInfo
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
import { getResource, IntlString, translate } from '@hcengineering/platform' import { getResource, IntlString, translate } from '@hcengineering/platform'
import contact, { formatName, PersonAccount } from '@hcengineering/contact' import contact, { formatName, PersonAccount } from '@hcengineering/contact'
import { DocUpdateMessage } from '@hcengineering/activity' import { DocUpdateMessage } from '@hcengineering/activity'
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { UserInfo, NotifyResult } from './types' import { NotifyResult } from './types'
/** /**
* @public * @public
@ -107,45 +110,65 @@ export async function shouldNotifyCommon (
): Promise<NotifyResult> { ): Promise<NotifyResult> {
const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0] const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0]
const emailTypes: BaseNotificationType[] = []
let allowed = false
let push = false
if (type === undefined) { if (type === undefined) {
return { allowed, emails: emailTypes, push } return new Map()
} }
if (await isAllowed(control, user as Ref<PersonAccount>, type._id, notification.providers.PlatformNotification)) { const result = new Map<Ref<NotificationProvider>, BaseNotificationType[]>()
allowed = true const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {})
}
if (await isAllowed(control, user as Ref<PersonAccount>, type._id, notification.providers.BrowserNotification)) { for (const provider of providers) {
push = true const allowed = await isAllowed(control, user as Ref<PersonAccount>, type, provider)
} if (allowed) {
if (await isAllowed(control, user as Ref<PersonAccount>, type._id, notification.providers.EmailNotification)) { const cur = result.get(provider._id) ?? []
emailTypes.push(type) result.set(provider._id, [...cur, type])
}
} }
return { allowed, push, emails: emailTypes } return result
} }
export async function isAllowed ( export async function isAllowed (
control: TriggerControl, control: TriggerControl,
receiver: Ref<PersonAccount>, receiver: Ref<PersonAccount>,
typeId: Ref<BaseNotificationType>, type: BaseNotificationType,
providerId: Ref<NotificationProvider> provider: NotificationProvider
): Promise<boolean> { ): Promise<boolean> {
const settings = await control.queryFind(notification.class.NotificationSetting, {}) const providersSettings = await control.queryFind(notification.class.NotificationProviderSetting, {})
const setting = settings.find((p) => p.attachedTo === providerId && p.type === typeId && p.modifiedBy === receiver) 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) { if (setting !== undefined) {
return setting.enabled return setting.enabled
} }
const type = (
await control.modelDb.findAll(notification.class.BaseNotificationType, { if (providerDefaults.some((it) => it.provider === provider._id && it.enabledTypes.includes(type._id))) {
_id: typeId return true
}) }
)[0]
if (type === undefined) return false if (type === undefined) return false
return type.providers[providerId] ?? false
return type.defaultEnabled
} }
export async function isShouldNotifyTx ( export async function isShouldNotifyTx (
@ -158,10 +181,6 @@ export async function isShouldNotifyTx (
isSpace: boolean, isSpace: boolean,
docUpdateMessage?: DocUpdateMessage docUpdateMessage?: DocUpdateMessage
): Promise<NotifyResult> { ): Promise<NotifyResult> {
let allowed = false
let push = false
const emailTypes: NotificationType[] = []
const types = await getMatchedTypes( const types = await getMatchedTypes(
control, control,
tx, tx,
@ -170,8 +189,9 @@ export async function isShouldNotifyTx (
isSpace, isSpace,
docUpdateMessage?.attributeUpdates?.attrKey docUpdateMessage?.attributeUpdates?.attrKey
) )
const modifiedAccount = await getPersonAccountById(tx.modifiedBy, control) const modifiedAccount = await getPersonAccountById(tx.modifiedBy, control)
const result = new Map<Ref<NotificationProvider>, BaseNotificationType[]>()
const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {})
for (const type of types) { for (const type of types) {
if ( if (
@ -190,21 +210,17 @@ export async function isShouldNotifyTx (
if (!res) continue if (!res) continue
} }
} }
if (await isAllowed(control, user._id, type._id, notification.providers.PlatformNotification)) { for (const provider of providers) {
allowed = true const allowed = await isAllowed(control, user._id, type, provider)
}
if (await isAllowed(control, user._id, type._id, notification.providers.BrowserNotification)) { if (allowed) {
push = true const cur = result.get(provider._id) ?? []
} result.set(provider._id, [...cur, type])
if (await isAllowed(control, user._id, type._id, notification.providers.EmailNotification)) { }
emailTypes.push(type)
} }
} }
return {
allowed, return result
push,
emails: emailTypes
}
} }
async function getMatchedTypes ( async function getMatchedTypes (
@ -357,6 +373,24 @@ async function getFallbackNotificationFullfillment (
} }
break break
} }
} else if (originTx._class === core.class.TxCollectionCUD && tx._class === core.class.TxCreateDoc) {
const createTx = tx as TxCreateDoc<Doc>
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<Doc>
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 } return { title, body, intlParams, intlParamsNotLocalized }

View File

@ -16,7 +16,13 @@
import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact'
import { Account, Class, Doc, Mixin, Ref, Tx, TxCUD } from '@hcengineering/core' 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 { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform'
import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core' import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core'
@ -129,7 +135,28 @@ export interface NotificationPresenter extends Class<Doc> {
presenter: Resource<NotificationContentProvider> presenter: Resource<NotificationContentProvider>
} }
export interface UserInfo {
_id: Ref<Account>
account?: PersonAccount
person?: Person
}
export type NotificationProviderFunc = (
control: TriggerControl,
types: BaseNotificationType[],
object: Doc,
data: InboxNotification,
receiver: UserInfo,
sender: UserInfo
) => Promise<Tx[]>
export interface NotificationProviderResources extends Doc {
provider: Ref<NotificationProvider>
fn: Resource<NotificationProviderFunc>
}
export const NOTIFICATION_BODY_SIZE = 50 export const NOTIFICATION_BODY_SIZE = 50
export const NOTIFICATION_TITLE_SIZE = 30
/** /**
* @public * @public
@ -140,6 +167,9 @@ export default plugin(serverNotificationId, {
PushPrivateKey: '' as Metadata<string>, PushPrivateKey: '' as Metadata<string>,
PushSubject: '' as Metadata<string> PushSubject: '' as Metadata<string>
}, },
class: {
NotificationProviderResources: '' as Ref<Class<NotificationProviderResources>>
},
mixin: { mixin: {
HTMLPresenter: '' as Ref<Mixin<HTMLPresenter>>, HTMLPresenter: '' as Ref<Mixin<HTMLPresenter>>,
TextPresenter: '' as Ref<Mixin<TextPresenter>>, TextPresenter: '' as Ref<Mixin<TextPresenter>>,

View File

@ -164,8 +164,7 @@ async function getRequestNotificationTx (tx: TxCollectionCUD<Doc, Request>, cont
senderInfo, senderInfo,
{ isOwn: true, isSpace: false, shouldUpdateTimestamp: true }, { isOwn: true, isSpace: false, shouldUpdateTimestamp: true },
notifyContexts, notifyContexts,
messages, messages
new Map()
) )
res.push(...txes) res.push(...txes)
} }

View File

@ -42,6 +42,7 @@
"@hcengineering/notification": "^0.6.23", "@hcengineering/notification": "^0.6.23",
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/server-core": "^0.6.1", "@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0", "@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/task": "^0.6.20", "@hcengineering/task": "^0.6.20",
"@hcengineering/tracker": "^0.6.24", "@hcengineering/tracker": "^0.6.24",

View File

@ -37,14 +37,14 @@ import type { TriggerControl } from '@hcengineering/server-core'
import { import {
getCommonNotificationTxes, getCommonNotificationTxes,
getNotificationContent, getNotificationContent,
isShouldNotifyTx, isShouldNotifyTx
UserInfo
} from '@hcengineering/server-notification-resources' } from '@hcengineering/server-notification-resources'
import task, { makeRank } from '@hcengineering/task' import task, { makeRank } from '@hcengineering/task'
import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/text' import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/text'
import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker' import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker'
import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time' import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time'
import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time' import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time'
import { UserInfo } from '@hcengineering/server-notification'
/** /**
* @public * @public

View File

@ -36,7 +36,7 @@ export class NotificationsPage {
documents = (): Locator => this.page.getByRole('button', { name: 'Documents' }) documents = (): Locator => this.page.getByRole('button', { name: 'Documents' })
requests = (): Locator => this.page.getByRole('button', { name: 'Requests' }) requests = (): Locator => this.page.getByRole('button', { name: 'Requests' })
todos = (): Locator => this.page.getByRole('button', { name: "Todo's" }) 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) { constructor (page: Page) {
this.page = page this.page = page