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'
import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction } from '@hcengineering/model-view'
import notification from '@hcengineering/notification'
import view from '@hcengineering/model-view'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import activity from './plugin'
import { buildActions } from './actions'
import { buildNotifications } from './notification'
export { activityId } from '@hcengineering/activity'
export { activityOperation, migrateMessagesSpace } from './migration'
@ -335,22 +336,10 @@ export function createModel (builder: Builder): void {
activity.ids.ReactionAddedActivityViewlet
)
builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['createdBy', 'repliedPersons']
})
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['createdBy', 'repliedPersons']
})
builder.mixin(activity.class.ActivityMessage, core.class.Class, view.mixin.ObjectPanel, {
component: view.component.AttachedDocPanel
})
builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.NotificationContextPresenter, {
labelPresenter: activity.component.ActivityMessageNotificationLabel
})
builder.mixin<Class<DocUpdateMessage>, IndexingConfiguration<DocUpdateMessage>>(
activity.class.DocUpdateMessage,
core.class.Class,
@ -371,24 +360,6 @@ export function createModel (builder: Builder): void {
}
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
generated: false,
label: activity.string.Reactions,
group: activity.ids.ActivityNotificationGroup,
txClasses: [core.class.TxCreateDoc],
objectClass: activity.class.Reaction,
providers: {
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: false
}
},
activity.ids.AddReactionNotification
)
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_ACTIVITY,
indexes: [
@ -405,112 +376,8 @@ export function createModel (builder: Builder): void {
]
})
createAction(
builder,
{
action: activity.actionImpl.AddReaction,
label: activity.string.AddReaction,
icon: activity.icon.Emoji,
input: 'focus',
category: activity.category.Activity,
target: activity.class.ActivityMessage,
inline: true,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.AddReactionAction
)
createAction(
builder,
{
action: activity.actionImpl.SaveForLater,
label: activity.string.SaveForLater,
icon: activity.icon.Bookmark,
input: 'focus',
inline: true,
actionProps: {
size: 'x-small'
},
category: activity.category.Activity,
target: activity.class.ActivityMessage,
visibilityTester: activity.function.CanSaveForLater,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.SaveForLaterAction
)
createAction(
builder,
{
action: activity.actionImpl.RemoveFromSaved,
label: activity.string.RemoveFromLater,
icon: activity.icon.BookmarkFilled,
input: 'focus',
inline: true,
actionProps: {
iconProps: {
fill: 'var(--global-accent-TextColor)'
}
},
category: activity.category.Activity,
target: activity.class.ActivityMessage,
visibilityTester: activity.function.CanRemoveFromSaved,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.RemoveFromLaterAction
)
createAction(
builder,
{
action: activity.actionImpl.PinMessage,
label: view.string.Pin,
icon: view.icon.Pin,
input: 'focus',
inline: true,
category: activity.category.Activity,
target: activity.class.ActivityMessage,
visibilityTester: activity.function.CanPinMessage,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.PinMessageAction
)
createAction(
builder,
{
action: activity.actionImpl.UnpinMessage,
label: view.string.Unpin,
icon: view.icon.Pin,
input: 'focus',
inline: true,
actionProps: {
iconProps: {
fill: 'var(--global-accent-TextColor)'
}
},
category: activity.category.Activity,
target: activity.class.ActivityMessage,
visibilityTester: activity.function.CanUnpinMessage,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.UnpinMessageAction
)
buildActions(builder)
buildNotifications(builder)
}
export default activity

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}',
subjectTemplate: 'Reminder: {doc}'
},
providers: {
[notification.providers.PlatformNotification]: true,
[notification.providers.EmailNotification]: false
}
defaultEnabled: false
},
calendar.ids.ReminderNotification
)
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: notification.providers.InboxNotificationProvider,
ignoredTypes: [],
enabledTypes: [calendar.ids.ReminderNotification]
})
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,

View File

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

View File

@ -34,6 +34,7 @@ import {
import activity, { migrateMessagesSpace, DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import notification from '@hcengineering/notification'
import contactPlugin, { type PersonAccount } from '@hcengineering/contact'
import { DOMAIN_NOTIFICATION } from '@hcengineering/model-notification'
import chunter from './plugin'
import { DOMAIN_CHUNTER } from './index'
@ -187,6 +188,9 @@ async function removeOldClasses (client: MigrationClient): Promise<void> {
for (const _class of classes) {
await client.deleteMany(DOMAIN_CHUNTER, { _class })
await client.deleteMany(DOMAIN_ACTIVITY, { attachedToClass: _class })
await client.deleteMany(DOMAIN_ACTIVITY, { objectClass: _class })
await client.deleteMany(DOMAIN_NOTIFICATION, { attachedToClass: _class })
await client.deleteMany(DOMAIN_TX, { objectClass: _class })
await client.deleteMany(DOMAIN_TX, { 'tx.objectClass': _class })
}
@ -226,7 +230,7 @@ export const chunterOperation: MigrateOperation = {
}
},
{
state: 'remove-old-classes',
state: 'remove-old-classes-v1',
func: async (client) => {
await removeOldClasses(client)
}

View File

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

View File

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

View File

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

View File

@ -35,6 +35,8 @@ import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
import notification from '@hcengineering/model-notification'
import view, { createAction } from '@hcengineering/model-view'
import setting from '@hcengineering/setting'
import love from '@hcengineering/model-love'
import gmail from './plugin'
export { gmailId } from '@hcengineering/gmail'
@ -234,10 +236,7 @@ export function createModel (builder: Builder): void {
objectClass: gmail.class.Message,
group: gmail.ids.EmailNotificationGroup,
allowedForAuthor: true,
providers: {
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: false
}
defaultEnabled: false
},
gmail.ids.EmailNotification
)
@ -258,4 +257,36 @@ export function createModel (builder: Builder): void {
{ createdOn: -1 }
]
})
builder.createDoc(
notification.class.NotificationProvider,
core.space.Model,
{
icon: contact.icon.Email,
label: gmail.string.Email,
description: gmail.string.EmailNotificationsDescription,
defaultEnabled: true,
canDisable: true,
depends: notification.providers.InboxNotificationProvider,
order: 30
},
gmail.providers.EmailNotificationProvider
)
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: notification.providers.InboxNotificationProvider,
ignoredTypes: [],
enabledTypes: [gmail.ids.EmailNotification]
})
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: gmail.providers.EmailNotificationProvider,
ignoredTypes: [
gmail.ids.EmailNotification,
notification.ids.CollaboratoAddNotification,
love.ids.InviteNotification,
love.ids.KnockNotification
],
enabledTypes: []
})
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import core, { type Ref, type Space } from '@hcengineering/core'
import core, { type Class, type Doc, type Ref, type Space } from '@hcengineering/core'
import { gmailId } from '@hcengineering/gmail'
import {
migrateSpace,
@ -23,6 +23,24 @@ import {
type MigrationUpgradeClient
} from '@hcengineering/model'
import { DOMAIN_GMAIL } from '.'
import notification from '@hcengineering/notification'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import gmail from './plugin'
async function migrateSettings (client: MigrationClient): Promise<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 = {
async migrate (client: MigrationClient): Promise<void> {
@ -32,6 +50,10 @@ export const gmailOperation: MigrateOperation = {
func: async (client: MigrationClient) => {
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
txClasses: [],
objectClass: hr.class.Request,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: false,
[notification.providers.PlatformNotification]: true
},
defaultEnabled: true,
templates: {
textTemplate: 'New request: {doc}',
htmlTemplate: 'New request: {doc}',
@ -508,11 +504,7 @@ export function createModel (builder: Builder): void {
// will be created with different trigger
txClasses: [],
objectClass: hr.class.Request,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
defaultEnabled: true,
templates: {
textTemplate: 'Request updated: {doc}',
htmlTemplate: 'Request updated: {doc}',
@ -533,11 +525,7 @@ export function createModel (builder: Builder): void {
// will be created with different trigger
txClasses: [],
objectClass: hr.class.Request,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
defaultEnabled: true,
templates: {
textTemplate: 'Request removed: {doc}',
htmlTemplate: 'Request removed: {doc}',
@ -558,11 +546,7 @@ export function createModel (builder: Builder): void {
// will be created with different trigger
txClasses: [],
objectClass: hr.class.PublicHoliday,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
defaultEnabled: true,
templates: {
textTemplate: 'New public holiday: {doc}',
htmlTemplate: 'New public holiday: {doc}',

View File

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

View File

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

View File

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

View File

@ -69,15 +69,17 @@ import {
type NotificationObjectPresenter,
type NotificationPreferencesGroup,
type NotificationPreview,
type NotificationProvider,
type NotificationSetting,
type NotificationStatus,
type NotificationTemplate,
type NotificationType,
type PushSubscription,
type PushSubscriptionKeys
type PushSubscriptionKeys,
type NotificationProvider,
type NotificationProviderSetting,
type NotificationTypeSetting,
type NotificationProviderDefaults
} from '@hcengineering/notification'
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import { type Asset, type IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { type AnyComponent, type Location } from '@hcengineering/ui/src/types'
@ -112,7 +114,7 @@ export class TBaseNotificationType extends TDoc implements BaseNotificationType
generated!: boolean
label!: IntlString
group!: Ref<NotificationGroup>
providers!: Record<Ref<NotificationProvider>, boolean>
defaultEnabled!: boolean
hidden!: boolean
templates?: NotificationTemplate
}
@ -142,20 +144,19 @@ export class TNotificationPreferencesGroup extends TDoc implements NotificationP
presenter!: AnyComponent
}
@Model(notification.class.NotificationProvider, core.class.Doc, DOMAIN_MODEL)
export class TNotificationProvider extends TDoc implements NotificationProvider {
label!: IntlString
depends?: Ref<NotificationProvider>
onChange?: Resource<(value: boolean) => Promise<boolean>>
}
@Model(notification.class.NotificationSetting, preference.class.Preference)
export class TNotificationSetting extends TPreference implements NotificationSetting {
@Model(notification.class.NotificationTypeSetting, preference.class.Preference)
export class TNotificationTypeSetting extends TPreference implements NotificationTypeSetting {
declare attachedTo: Ref<TNotificationProvider>
type!: Ref<BaseNotificationType>
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)
export class TClassCollaborators extends TClass {
fields!: string[]
@ -284,6 +285,26 @@ export class TActivityNotificationViewlet extends TDoc implements ActivityNotifi
presenter!: AnyComponent
}
@Model(notification.class.NotificationProvider, core.class.Doc)
export class TNotificationProvider extends TDoc implements NotificationProvider {
icon!: Asset
label!: IntlString
description!: IntlString
defaultEnabled!: boolean
order!: number
depends?: Ref<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({
pinContext: {
action: notification.actionImpl.PinDocNotifyContext,
@ -311,8 +332,6 @@ export function createModel (builder: Builder): void {
builder.createModel(
TBrowserNotification,
TNotificationType,
TNotificationProvider,
TNotificationSetting,
TNotificationGroup,
TNotificationPreferencesGroup,
TClassCollaborators,
@ -328,44 +347,11 @@ export function createModel (builder: Builder): void {
TBaseNotificationType,
TCommonNotificationType,
TMentionInboxNotification,
TPushSubscription
)
builder.createDoc(
notification.class.NotificationProvider,
core.space.Model,
{
label: notification.string.Inbox
},
notification.providers.PlatformNotification
)
builder.createDoc(
notification.class.NotificationProvider,
core.space.Model,
{
label: notification.string.Push,
depends: notification.providers.PlatformNotification
},
notification.providers.BrowserNotification
)
builder.createDoc(
notification.class.NotificationProvider,
core.space.Model,
{
label: notification.string.Sound
},
notification.providers.SoundNotification
)
builder.createDoc(
notification.class.NotificationProvider,
core.space.Model,
{
label: notification.string.EmailNotification
},
notification.providers.EmailNotification
TPushSubscription,
TNotificationProvider,
TNotificationProviderSetting,
TNotificationTypeSetting,
TNotificationProviderDefaults
)
builder.createDoc(
@ -434,9 +420,7 @@ export function createModel (builder: Builder): void {
group: notification.ids.NotificationGroup,
txClasses: [],
objectClass: notification.mixin.Collaborators,
providers: {
[notification.providers.PlatformNotification]: true
}
defaultEnabled: true
},
notification.ids.CollaboratoAddNotification
)
@ -560,14 +544,10 @@ export function createModel (builder: Builder): void {
generated: false,
hidden: false,
group: notification.ids.NotificationGroup,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
defaultEnabled: true,
templates: {
textTemplate: '{sender} mentioned you in {doc} {data}',
htmlTemplate: '<p>{sender}</b> mentioned you in {doc}</p> {data}',
textTemplate: '{sender} mentioned you in {doc} {message}',
htmlTemplate: '<p>{sender}</b> mentioned you in {doc}</p> {message}',
subjectTemplate: 'You were mentioned in {doc}'
}
},
@ -654,6 +634,7 @@ export function createModel (builder: Builder): void {
indexes: []
}
)
builder.mixin<Class<InboxNotification>, IndexingConfiguration<InboxNotification>>(
notification.class.InboxNotification,
core.class.Class,
@ -672,6 +653,73 @@ export function createModel (builder: Builder): void {
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 (
@ -687,6 +735,8 @@ export function generateClassNotificationTypes (
hierarchy.isDerived(_class, core.class.AttachedDoc) ? core.class.AttachedDoc : core.class.Doc
)
const filtered = Array.from(attributes.values()).filter((p) => p.hidden !== true && p.readonly !== true)
const enabledInboxTypes: Ref<BaseNotificationType>[] = []
for (const attribute of filtered) {
if (ignoreKeys.includes(attribute.name)) continue
const isCollection: boolean = core.class.Collection === attribute.type._class
@ -704,9 +754,11 @@ export function generateClassNotificationTypes (
objectClass,
txClasses,
hidden: false,
providers: {
[notification.providers.PlatformNotification]: defaultEnabled.includes(attribute.name),
[notification.providers.BrowserNotification]: false
defaultEnabled: false,
templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{doc} updated'
},
label: attribute.label
}
@ -715,5 +767,17 @@ export function generateClassNotificationTypes (
}
const id = `${notification.class.NotificationType}_${_class}_${attribute.name}` as Ref<NotificationType>
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
} from '@hcengineering/model'
import notification, { notificationId, type DocNotifyContext } from '@hcengineering/notification'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import { DOMAIN_NOTIFICATION } from './index'
@ -67,6 +68,32 @@ export async function removeNotifications (
}
}
export async function migrateSettings (client: MigrationClient): Promise<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 = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, notificationId, [
@ -96,6 +123,10 @@ export const notificationOperation: MigrateOperation = {
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, {
fields: ['createdBy']
fields: ['createdBy', 'assignee']
})
builder.mixin(recruit.mixin.Candidate, core.class.Mixin, view.mixin.ObjectFactory, {
@ -1314,11 +1314,7 @@ export function createModel (builder: Builder): void {
htmlTemplate: '<p>{doc} was assigned to you by {sender}</p>',
subjectTemplate: '{doc} was assigned to you'
},
providers: {
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.EmailNotification]: true
}
defaultEnabled: true
},
recruit.ids.AssigneeNotification
)
@ -1354,9 +1350,11 @@ export function createModel (builder: Builder): void {
txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc],
objectClass: recruit.class.Applicant,
spaceSubscribe: true,
providers: {
[notification.providers.PlatformNotification]: false,
[notification.providers.BrowserNotification]: false
defaultEnabled: false,
templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
}
},
recruit.ids.ApplicationCreateNotification

View File

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

View File

@ -134,11 +134,7 @@ export function createModel (builder: Builder): void {
group: request.ids.RequestNotificationGroup,
label: request.string.Request,
allowedForAuthor: true,
providers: {
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true,
[notification.providers.EmailNotification]: true
},
defaultEnabled: true,
templates: {
textTemplate: '{sender} sent you a {doc}',
htmlTemplate: '<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 core from '@hcengineering/core/src/component'
import serverActivity from '@hcengineering/server-activity'
import serverNotification from '@hcengineering/server-notification'
import activity from '@hcengineering/activity'
export { activityServerOperation } from './migration'
export { serverActivityId } from '@hcengineering/server-activity'
export function createModel (builder: Builder): void {
builder.mixin(activity.class.Reaction, core.class.Class, serverNotification.mixin.NotificationPresenter, {
presenter: serverActivity.function.ReactionNotificationContentProvider
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverActivity.trigger.OnReactionChanged,
txMatch: {

View File

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

View File

@ -14,12 +14,12 @@
// limitations under the License.
//
import { type Builder, Mixin } from '@hcengineering/model'
import { type Builder, Mixin, Model } from '@hcengineering/model'
import core, { type Account, type Doc, type Ref, type Tx } from '@hcengineering/core'
import { TClass } from '@hcengineering/model-core'
import { TClass, TDoc } from '@hcengineering/model-core'
import { TNotificationType } from '@hcengineering/model-notification'
import notification, { type NotificationType } from '@hcengineering/notification'
import notification, { type NotificationProvider, type NotificationType } from '@hcengineering/notification'
import { type Resource } from '@hcengineering/platform'
import serverCore, { type TriggerControl } from '@hcengineering/server-core'
import serverNotification, {
@ -28,7 +28,9 @@ import serverNotification, {
type Presenter,
type TextPresenter,
type TypeMatch,
type NotificationContentProvider
type NotificationContentProvider,
type NotificationProviderResources,
type NotificationProviderFunc
} from '@hcengineering/server-notification'
export { serverNotificationId } from '@hcengineering/server-notification'
@ -55,8 +57,20 @@ export class TTypeMatch extends TNotificationType implements TypeMatch {
>
}
@Model(serverNotification.class.NotificationProviderResources, core.class.Doc)
export class TNotificationProviderResources extends TDoc implements NotificationProviderResources {
provider!: Ref<NotificationProvider>
fn!: Resource<NotificationProviderFunc>
}
export function createModel (builder: Builder): void {
builder.createModel(THTMLPresenter, TTextPresenter, TTypeMatch, TNotificationPresenter)
builder.createModel(
THTMLPresenter,
TTextPresenter,
TTypeMatch,
TNotificationPresenter,
TNotificationProviderResources
)
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.OnActivityNotificationViewed,

View File

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

View File

@ -199,13 +199,17 @@ export function createModel (builder: Builder): void {
txClasses: [core.class.TxCreateDoc],
objectClass: telegram.class.Message,
group: telegram.ids.NotificationGroup,
providers: {
[notification.providers.PlatformNotification]: true
}
defaultEnabled: false
},
telegram.ids.NewMessageNotification
)
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: notification.providers.InboxNotificationProvider,
ignoredTypes: [],
enabledTypes: [telegram.ids.NewMessageNotification]
})
builder.mixin(telegram.class.Message, core.class.Class, core.mixin.FullTextSearchContext, {
parentPropagate: false,
childProcessingAllowed: true

View File

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

View File

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

View File

@ -587,10 +587,7 @@ function defineTrainingRequest (builder: Builder): void {
group: training.notification.TrainingGroup,
txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc],
objectClass: training.class.TrainingRequest,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.PlatformNotification]: true
},
defaultEnabled: true,
templates: {
textTemplate: '{sender} sent you a training request {doc}',
htmlTemplate: '<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 Asset, getMetadata } from '@hcengineering/platform'
import { type Asset, getMetadata, getResource } from '@hcengineering/platform'
import { getClient } from '.'
import notification from '@hcengineering/notification'
const sounds = new Map<Asset, AudioBufferSourceNode>()
const context = new AudioContext()
export async function prepareSound (key: string, _class?: Ref<Class<Doc>>, loop = false, play = false): Promise<void> {
const notificationType =
_class !== undefined
? getClient().getModel().findAllSync(notification.class.NotificationType, { objectClass: _class })
: undefined
const notAllowed = notificationType?.[0].providers[notification.providers.SoundNotification] === false
if (notificationType === undefined || notAllowed) {
return
}
if (_class === undefined) return
const client = getClient()
const notificationType = client
.getModel()
.findAllSync(notification.class.NotificationType, { objectClass: _class })[0]
if (notificationType === undefined) return
const isAllowedFn = await getResource(notification.function.IsNotificationAllowed)
const allowed: boolean = isAllowedFn(notificationType, notification.providers.SoundNotificationProvider)
if (!allowed) return
try {
const soundUrl = getMetadata(key as Asset) as string
const audioBuffer = await fetch(soundUrl)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@
"NewIncomingMessage": "Sent you a new email",
"ConfigLabel": "Email",
"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",
"ConfigLabel": "Correo Electrónico",
"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",
"ConfigLabel": "Email",
"ConfigDescription": "Extension pour l'intégration des emails Gmail",
"GooglePrivacy": "L'utilisation et le transfert des informations reçues des API Google par Huly à toute autre application respecteront les <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",
"ConfigLabel": "Email",
"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": "Прислал вам новое сообщение",
"ConfigLabel": "Электронная почта",
"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": "给您发送了一封新邮件",
"ConfigLabel": "电子邮件",
"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 type { Account, AttachedDoc, Class, Doc, Ref, Timestamp } from '@hcengineering/core'
import { NotificationType } from '@hcengineering/notification'
import { NotificationProvider, NotificationType } from '@hcengineering/notification'
import type { IntlString, Plugin } from '@hcengineering/platform'
import { Metadata, plugin } from '@hcengineering/platform'
import type { Handler, IntegrationType } from '@hcengineering/setting'
@ -89,7 +89,8 @@ export default plugin(gmailId, {
},
string: {
From: '' as IntlString,
To: '' as IntlString
To: '' as IntlString,
EmailNotificationsDescription: '' as IntlString
},
integrationType: {
Gmail: '' as Ref<IntegrationType>
@ -108,5 +109,8 @@ export default plugin(gmailId, {
},
metadata: {
GmailURL: '' as Metadata<string>
},
providers: {
EmailNotificationProvider: '' as Ref<NotificationProvider>
}
})

View File

@ -49,6 +49,12 @@
"Unreads": "Unreads",
"EnablePush": "Enable push notifications",
"NotificationBlockedInBrowser": "Notifications are blocked in your browser. Please enable notifications in your browser settings",
"Sound": "Sound"
"General": "General",
"InboxNotificationsDescription": "Receive personal notifications in your Huly inbox.",
"PushNotificationsDescription": "Receive personal notifications on desktop.",
"CommonNotificationCollectionAdded": "{senderName} added {collection}",
"CommonNotificationCollectionRemoved": "{senderName} removed {collection}",
"Sound": "Sound",
"SoundNotificationsDescription": "Receive sound notifications for events."
}
}

View File

@ -48,6 +48,12 @@
"Push": "Push",
"EnablePush": "Habilitar notificaciones push",
"NotificationBlockedInBrowser": "Las notificaciones están bloqueadas en tu navegador. Por favor, habilita las notificaciones en la configuración de tu navegador.",
"Sound": "Sonido"
"General": "General",
"InboxNotificationsDescription": "Reciba notificaciones personales en su bandeja de entrada de Huly.",
"PushNotificationsDescription": "Reciba notificaciones personales en el escritorio.",
"Sound": "Sonido",
"SoundNotificationsDescription": "Reciba notificaciones de sonido para eventos.",
"CommonNotificationCollectionAdded": "{senderName} añadió {collection}",
"CommonNotificationCollectionRemoved": "{senderName} eliminó {collection}"
}
}

View File

@ -49,6 +49,12 @@
"Unreads": "Non lus",
"EnablePush": "Activer les notifications push",
"NotificationBlockedInBrowser": "Les notifications sont bloquées dans votre navigateur. Veuillez activer les notifications dans les paramètres de votre navigateur",
"Sound": "Son"
"General": "Général",
"InboxNotificationsDescription": "Recevez des notifications personnelles dans votre boîte de réception Huly.",
"PushNotificationsDescription": "Recevez des notifications personnelles sur le bureau.",
"Sound": "Son",
"SoundNotificationsDescription": "Recevez des notifications sonores pour les événements.",
"CommonNotificationCollectionAdded": "{senderName} a ajouté {collection}",
"CommonNotificationCollectionRemoved": "{senderName} a supprimé {collection}"
}
}

View File

@ -48,6 +48,12 @@
"Push": "Push",
"EnablePush": "Ativar notificações push",
"NotificationBlockedInBrowser": "Notificações bloqueadas no navegador. Por favor habilite las notificaciones en la configuración de su navegador.",
"Sound": "Som"
"General": "Geral",
"InboxNotificationsDescription": "Receba notificações pessoais em sua caixa de entrada do Huly.",
"PushNotificationsDescription": "Receba notificações pessoais no desktop.",
"Sound": "Som",
"SoundNotificationsDescription": "Receba notificações sonoras para eventos.",
"CommonNotificationCollectionAdded": "{senderName} adicionou {collection}",
"CommonNotificationCollectionRemoved": "{senderName} removeu {collection}"
}
}

View File

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

View File

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

View File

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

View File

@ -43,9 +43,13 @@ import notification, {
type DisplayInboxNotification,
type DocNotifyContext,
type InboxNotification,
type MentionInboxNotification
type MentionInboxNotification,
type BaseNotificationType,
type NotificationProvider,
type NotificationProviderSetting,
type NotificationTypeSetting
} from '@hcengineering/notification'
import { MessageBox, getClient } from '@hcengineering/presentation'
import { MessageBox, getClient, createQuery } from '@hcengineering/presentation'
import {
getCurrentLocation,
getLocation,
@ -65,6 +69,23 @@ import { getObjectLinkId } from '@hcengineering/view-resources'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
import { type InboxData, type InboxNotificationsFilter } from './types'
export const providersSettings = writable<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> {
if (docNotifyContext.hidden) {
return false
@ -777,3 +798,39 @@ export function notificationsComparator (notifications1: InboxNotification, noti
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??)
hidden: boolean
group: Ref<NotificationGroup>
// allowed providers and default value for it
providers: Record<Ref<NotificationProvider>, boolean>
defaultEnabled: boolean
// templates for email (and browser/push?)
templates?: NotificationTemplate
}
@ -152,19 +151,30 @@ export interface NotificationType extends BaseNotificationType {
export interface CommonNotificationType extends BaseNotificationType {}
/**
* @public
*/
export interface NotificationProvider extends Doc {
label: IntlString
description: IntlString
icon: Asset
defaultEnabled: boolean
depends?: Ref<NotificationProvider>
onChange?: Resource<(value: boolean) => Promise<boolean>>
canDisable: boolean
ignoreAll?: boolean
order: number
}
/**
* @public
*/
export interface NotificationSetting extends Preference {
export interface NotificationProviderDefaults extends Doc {
provider: Ref<NotificationProvider>
excludeIgnore?: Ref<BaseNotificationType>[]
ignoredTypes: Ref<BaseNotificationType>[]
enabledTypes: Ref<BaseNotificationType>[]
}
export interface NotificationProviderSetting extends Preference {
attachedTo: Ref<NotificationProvider>
enabled: boolean
}
export interface NotificationTypeSetting extends Preference {
attachedTo: Ref<NotificationProvider>
type: Ref<BaseNotificationType>
enabled: boolean
@ -329,8 +339,6 @@ const notification = plugin(notificationId, {
BaseNotificationType: '' as Ref<Class<BaseNotificationType>>,
NotificationType: '' as Ref<Class<NotificationType>>,
CommonNotificationType: '' as Ref<Class<CommonNotificationType>>,
NotificationProvider: '' as Ref<Class<NotificationProvider>>,
NotificationSetting: '' as Ref<Class<NotificationSetting>>,
NotificationGroup: '' as Ref<Class<NotificationGroup>>,
NotificationPreferencesGroup: '' as Ref<Class<NotificationPreferencesGroup>>,
DocNotifyContext: '' as Ref<Class<DocNotifyContext>>,
@ -338,7 +346,11 @@ const notification = plugin(notificationId, {
ActivityInboxNotification: '' as Ref<Class<ActivityInboxNotification>>,
CommonInboxNotification: '' as Ref<Class<CommonInboxNotification>>,
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: {
NotificationSettings: '' as Ref<Doc>,
@ -350,10 +362,9 @@ const notification = plugin(notificationId, {
PushPublicKey: '' as Metadata<string>
},
providers: {
PlatformNotification: '' as Ref<NotificationProvider>,
BrowserNotification: '' as Ref<NotificationProvider>,
EmailNotification: '' as Ref<NotificationProvider>,
SoundNotification: '' as Ref<NotificationProvider>
InboxNotificationProvider: '' as Ref<NotificationProvider>,
PushNotificationProvider: '' as Ref<NotificationProvider>,
SoundNotificationProvider: '' as Ref<NotificationProvider>
},
integrationType: {
MobileApp: '' as Ref<IntegrationType>
@ -364,7 +375,8 @@ const notification = plugin(notificationId, {
CollaboratorsChanged: '' as AnyComponent,
DocNotifyContextPresenter: '' as AnyComponent,
NotificationCollaboratorsChanged: '' as AnyComponent,
ReactionNotificationPresenter: '' as AnyComponent
ReactionNotificationPresenter: '' as AnyComponent,
GeneralPreferencesGroup: '' as AnyComponent
},
action: {
PinDocNotifyContext: '' as Ref<Action>,
@ -402,6 +414,12 @@ const notification = plugin(notificationId, {
YouAddedCollaborators: '' as IntlString,
YouRemovedCollaborators: '' as IntlString,
Push: '' as IntlString,
General: '' as IntlString,
InboxNotificationsDescription: '' as IntlString,
PushNotificationsDescription: '' as IntlString,
CommonNotificationCollectionAdded: '' as IntlString,
CommonNotificationCollectionRemoved: '' as IntlString,
SoundNotificationsDescription: '' as IntlString,
Sound: '' as IntlString
},
function: {
@ -410,6 +428,9 @@ const notification = plugin(notificationId, {
GetInboxNotificationsClient: '' as Resource<InboxNotificationsClientFactory>,
HasInboxNotifications: '' as Resource<
(notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>
>,
IsNotificationAllowed: '' as Resource<
(type: BaseNotificationType, providerId: Ref<NotificationProvider>) => boolean
>
},
resolver: {

View File

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

View File

@ -21,6 +21,7 @@ import core, {
CollaborativeDoc,
Data,
Doc,
generateId,
Hierarchy,
Ref,
Space,
@ -35,7 +36,7 @@ import core, {
TxUpdateDoc,
Type
} from '@hcengineering/core'
import notification, { MentionInboxNotification } from '@hcengineering/notification'
import notification, { CommonInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
import {
extractReferences,
markupToPmNode,
@ -52,7 +53,8 @@ import {
shouldNotifyCommon,
isShouldNotifyTx,
NotifyResult,
createPushFromInbox
applyNotificationProviders,
getNotificationContent
} from '@hcengineering/server-notification-resources'
async function getPersonAccount (person: Ref<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 messageNotifyResult = await getMessageNotifyResult(reference, receiver, control, originTx, doc)
if (messageNotifyResult.allowed) {
notifyResult.allowed = false
}
if (messageNotifyResult.push) {
notifyResult.push = false
}
if (messageNotifyResult.emails.length > 0) {
notifyResult.emails = []
}
const txes = await getCommonNotificationTxes(
control,
doc,
data,
receiverInfo,
senderInfo,
reference.srcDocId,
reference.srcDocClass,
space,
originTx.modifiedOn,
notifyResult,
notification.class.MentionInboxNotification
)
if (!notifyResult.allowed && notifyResult.push) {
const exists = (
await control.findAll(
notification.class.ActivityInboxNotification,
{ attachedTo: reference.attachedDocId as Ref<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)
}
for (const [provider] of messageNotifyResult.entries()) {
if (notifyResult.has(provider)) {
notifyResult.delete(provider)
}
}
res.push(...txes)
if (notifyResult.has(notification.providers.InboxNotificationProvider)) {
const txes = await getCommonNotificationTxes(
control,
doc,
data,
receiverInfo,
senderInfo,
reference.srcDocId,
reference.srcDocClass,
space,
originTx.modifiedOn,
notifyResult,
notification.class.MentionInboxNotification
)
res.push(...txes)
} else {
const context = (
await control.findAll(
notification.class.DocNotifyContext,
{ attachedTo: reference.srcDocId, user: receiver._id },
{ projection: { _id: 1 } }
)
)[0]
if (context !== undefined) {
const content = await getNotificationContent(originTx, receiver, senderInfo, doc, control)
const notificationData: CommonInboxNotification = {
...data,
...content,
docNotifyContext: context._id,
_id: generateId(),
_class: notification.class.CommonInboxNotification,
space,
modifiedOn: originTx.modifiedOn,
modifiedBy: sender._id
}
await applyNotificationProviders(
notificationData,
notifyResult,
reference.srcDocId,
reference.srcDocClass,
control,
res,
doc,
receiverInfo,
senderInfo
)
}
}
return res
}
@ -322,17 +328,17 @@ async function getMessageNotifyResult (
reference.attachedDocId === undefined ||
tx._class !== core.class.TxCreateDoc
) {
return { allowed: false, emails: [], push: false }
return new Map()
}
const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators)
if (mixin === undefined || !mixin.collaborators.includes(account._id)) {
return { allowed: false, emails: [], push: false }
return new Map()
}
if (!hierarchy.isDerived(reference.attachedDocClass, activity.class.ActivityMessage)) {
return { allowed: false, emails: [], push: false }
return new Map()
}
return await isShouldNotifyTx(control, tx, originTx, doc, account, false, false, undefined)

View File

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

View File

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

View File

@ -37,6 +37,7 @@ export default plugin(serverChunterId, {
CommentRemove: '' as Resource<ObjectDDParticipantFunc>,
ChannelHTMLPresenter: '' 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/platform": "^0.6.11",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/gmail": "^0.6.22"

View File

@ -13,10 +13,11 @@
// limitations under the License.
//
import contact, { Channel } from '@hcengineering/contact'
import contact, { Channel, formatName } from '@hcengineering/contact'
import {
Account,
Class,
concatLink,
Doc,
DocumentQuery,
FindOptions,
@ -29,7 +30,10 @@ import {
} from '@hcengineering/core'
import gmail, { Message } from '@hcengineering/gmail'
import { TriggerControl } from '@hcengineering/server-core'
import notification, { NotificationType } from '@hcengineering/notification'
import notification, { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification'
import serverNotification, { NotificationProviderFunc, UserInfo } from '@hcengineering/server-notification'
import { getContentByTemplate } from '@hcengineering/server-notification-resources'
import { getMetadata } from '@hcengineering/platform'
/**
* @public
@ -131,6 +135,94 @@ export async function IsIncomingMessage (
return message.incoming && message.sendOn > (doc.createdOn ?? doc.modifiedOn)
}
export async function sendEmailNotification (
text: string,
html: string,
subject: string,
receiver: string
): Promise<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
export default async () => ({
trigger: {
@ -138,6 +230,7 @@ export default async () => ({
},
function: {
IsIncomingMessage,
FindMessages
FindMessages,
SendEmailNotifications
}
})

View File

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

View File

@ -43,6 +43,8 @@
"@hcengineering/contact": "^0.6.24",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/gmail": "^0.6.22",
"@hcengineering/server-gmail-resources": "^0.6.0",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/hr": "^0.6.19"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,13 @@
import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact'
import { Account, Class, Doc, Mixin, Ref, Tx, TxCUD } from '@hcengineering/core'
import { NotificationContent, NotificationType } from '@hcengineering/notification'
import {
BaseNotificationType,
InboxNotification,
NotificationContent,
NotificationProvider,
NotificationType
} from '@hcengineering/notification'
import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform'
import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core'
@ -129,7 +135,28 @@ export interface NotificationPresenter extends Class<Doc> {
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_TITLE_SIZE = 30
/**
* @public
@ -140,6 +167,9 @@ export default plugin(serverNotificationId, {
PushPrivateKey: '' as Metadata<string>,
PushSubject: '' as Metadata<string>
},
class: {
NotificationProviderResources: '' as Ref<Class<NotificationProviderResources>>
},
mixin: {
HTMLPresenter: '' as Ref<Mixin<HTMLPresenter>>,
TextPresenter: '' as Ref<Mixin<TextPresenter>>,

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ export class NotificationsPage {
documents = (): Locator => this.page.getByRole('button', { name: 'Documents' })
requests = (): Locator => this.page.getByRole('button', { name: 'Requests' })
todos = (): Locator => this.page.getByRole('button', { name: "Todo's" })
chatMessageToggle = (): Locator => this.page.locator('div:nth-child(7) > .flex-between > .toggle > .toggle-switch')
chatMessageToggle = (): Locator => this.page.locator('.grid > div:nth-child(6)')
constructor (page: Page) {
this.page = page