Browser notifications (#5261)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-04-09 14:41:57 +05:00 committed by GitHub
parent 8b0627fc0f
commit 10f59e1028
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 376 additions and 254 deletions

View File

@ -380,7 +380,8 @@ export function createModel (builder: Builder): void {
txClasses: [core.class.TxCreateDoc],
objectClass: activity.class.Reaction,
providers: {
[notification.providers.PlatformNotification]: true
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: false
}
},
activity.ids.AddReactionNotification

View File

@ -456,6 +456,7 @@ export function createModel (builder: Builder, options = { addApplication: true
objectClass: chunter.class.ChatMessage,
providers: {
[notification.providers.EmailNotification]: false,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
group: chunter.ids.ChunterNotificationGroup,
@ -478,7 +479,8 @@ export function createModel (builder: Builder, options = { addApplication: true
txClasses: [core.class.TxCreateDoc],
objectClass: chunter.class.ChatMessage,
providers: {
[notification.providers.PlatformNotification]: true
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true
},
group: chunter.ids.ChunterNotificationGroup
},
@ -495,7 +497,8 @@ export function createModel (builder: Builder, options = { addApplication: true
txClasses: [core.class.TxCreateDoc],
objectClass: chunter.class.ThreadMessage,
providers: {
[notification.providers.PlatformNotification]: true
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true
},
group: chunter.ids.ChunterNotificationGroup
},

View File

@ -369,7 +369,8 @@ function defineDocument (builder: Builder): void {
txClasses: [core.class.TxUpdateDoc],
objectClass: document.class.Document,
providers: {
[notification.providers.PlatformNotification]: true
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: false
}
},
document.ids.ContentNotification

View File

@ -266,7 +266,8 @@ export function createModel (builder: Builder): void {
group: gmail.ids.EmailNotificationGroup,
allowedForAuthor: true,
providers: {
[notification.providers.PlatformNotification]: true
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: false
}
},
gmail.ids.EmailNotification

View File

@ -476,6 +476,7 @@ export function createModel (builder: Builder): void {
objectClass: hr.class.Request,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: false,
[notification.providers.PlatformNotification]: true
},
templates: {
@ -500,6 +501,7 @@ export function createModel (builder: Builder): void {
objectClass: hr.class.Request,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: {
@ -524,6 +526,7 @@ export function createModel (builder: Builder): void {
objectClass: hr.class.Request,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: {
@ -548,6 +551,7 @@ export function createModel (builder: Builder): void {
objectClass: hr.class.PublicHoliday,
providers: {
[notification.providers.EmailNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.PlatformNotification]: true
},
templates: {

View File

@ -411,6 +411,7 @@ export function createModel (builder: Builder): void {
},
providers: {
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.EmailNotification]: true
}
},
@ -462,7 +463,8 @@ export function createModel (builder: Builder): void {
objectClass: lead.class.Funnel,
spaceSubscribe: true,
providers: {
[notification.providers.PlatformNotification]: false
[notification.providers.PlatformNotification]: false,
[notification.providers.BrowserNotification]: false
}
},
lead.ids.LeadCreateNotification

View File

@ -17,6 +17,9 @@
import activity, { type ActivityMessage } from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import {
DOMAIN_MODEL,
Hierarchy,
IndexKind,
type Account,
type AttachedDoc,
type Class,
@ -25,59 +28,55 @@ import {
type Doc,
type DocumentQuery,
type Domain,
DOMAIN_MODEL,
Hierarchy,
IndexKind,
type Ref,
type Timestamp,
type Tx,
type TxCUD
type Tx
} from '@hcengineering/core'
import {
ArrOf,
type Builder,
Index,
Mixin,
Model,
Prop,
TypeBoolean,
TypeDate,
TypeIntlString,
TypeRef,
TypeString,
UX,
TypeBoolean,
TypeDate,
TypeIntlString
type Builder
} from '@hcengineering/model'
import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import core, { TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction, template } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import {
type DocUpdates,
type DocUpdateTx,
type InboxNotification,
notificationId,
type ActivityInboxNotification,
type ActivityNotificationViewlet,
type BaseNotificationType,
type BrowserNotification,
type CommonInboxNotification,
type CommonNotificationType,
type DocNotifyContext,
type Notification,
type DocUpdateTx,
type DocUpdates,
type InboxNotification,
type MentionInboxNotification,
type NotificationContextPresenter,
type NotificationGroup,
type NotificationObjectPresenter,
type NotificationPreferencesGroup,
type NotificationPreview,
type NotificationProvider,
type NotificationSetting,
type NotificationStatus,
type NotificationTemplate,
type NotificationType,
type NotificationObjectPresenter,
type ActivityInboxNotification,
type CommonInboxNotification,
type NotificationContextPresenter,
type ActivityNotificationViewlet,
type BaseNotificationType,
type CommonNotificationType,
notificationId,
type MentionInboxNotification
type NotificationType
} from '@hcengineering/notification'
import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform'
import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import { type AnyComponent, type Location } from '@hcengineering/ui/src/types'
import notification from './plugin'
@ -87,17 +86,13 @@ export { notification as default }
export const DOMAIN_NOTIFICATION = 'notification' as Domain
@Model(notification.class.Notification, core.class.AttachedDoc, DOMAIN_NOTIFICATION)
export class TNotification extends TAttachedDoc implements Notification {
@Prop(TypeRef(core.class.Tx), 'TX' as IntlString)
tx!: Ref<TxCUD<Doc>>
@Prop(TypeString(), 'Status' as IntlString)
status!: NotificationStatus
text!: string
type!: Ref<NotificationType>
@Model(notification.class.BrowserNotification, core.class.Doc, DOMAIN_NOTIFICATION)
export class TBrowserNotification extends TDoc implements BrowserNotification {
title!: string
body!: string
onClickLocation?: Location | undefined
user!: Ref<Account>
status!: NotificationStatus
}
@Model(notification.class.BaseNotificationType, core.class.Doc, DOMAIN_MODEL)
@ -138,7 +133,8 @@ export class TNotificationPreferencesGroup extends TDoc implements NotificationP
@Model(notification.class.NotificationProvider, core.class.Doc, DOMAIN_MODEL)
export class TNotificationProvider extends TDoc implements NotificationProvider {
label!: IntlString
default!: boolean
depends?: Ref<NotificationProvider>
onChange?: Resource<(value: boolean) => Promise<boolean>>
}
@Model(notification.class.NotificationSetting, preference.class.Preference)
@ -319,7 +315,7 @@ export const notificationActionTemplates = template({
export function createModel (builder: Builder): void {
builder.createModel(
TNotification,
TBrowserNotification,
TNotificationType,
TNotificationProvider,
TNotificationSetting,
@ -341,17 +337,6 @@ export function createModel (builder: Builder): void {
TMentionInboxNotification
)
// Temporarily disabled, we should think about it
// builder.createDoc(
// notification.class.NotificationProvider,
// core.space.Model,
// {
// label: notification.string.BrowserNotification,
// default: true
// },
// notification.ids.BrowserNotification
// )
builder.createDoc(
notification.class.NotificationProvider,
core.space.Model,
@ -361,6 +346,17 @@ export function createModel (builder: Builder): void {
notification.providers.PlatformNotification
)
builder.createDoc(
notification.class.NotificationProvider,
core.space.Model,
{
label: notification.string.Push,
depends: notification.providers.PlatformNotification,
onChange: notification.function.CheckPushPermission
},
notification.providers.BrowserNotification
)
builder.createDoc(
notification.class.NotificationProvider,
core.space.Model,
@ -696,7 +692,8 @@ export function generateClassNotificationTypes (
txClasses,
hidden: false,
providers: {
[notification.providers.PlatformNotification]: defaultEnabled.includes(attribute.name)
[notification.providers.PlatformNotification]: defaultEnabled.includes(attribute.name),
[notification.providers.BrowserNotification]: false
},
label: attribute.label
}

View File

@ -1304,6 +1304,7 @@ export function createModel (builder: Builder): void {
},
providers: {
[notification.providers.PlatformNotification]: true,
[notification.providers.BrowserNotification]: true,
[notification.providers.EmailNotification]: true
}
},
@ -1342,7 +1343,8 @@ export function createModel (builder: Builder): void {
objectClass: recruit.class.Applicant,
spaceSubscribe: true,
providers: {
[notification.providers.PlatformNotification]: false
[notification.providers.PlatformNotification]: false,
[notification.providers.BrowserNotification]: false
}
},
recruit.ids.ApplicationCreateNotification

View File

@ -133,6 +133,7 @@ export function createModel (builder: Builder): void {
label: request.string.Request,
allowedForAuthor: true,
providers: {
[notification.providers.BrowserNotification]: false,
[notification.providers.PlatformNotification]: true
}
},

View File

@ -42,6 +42,7 @@
"ArchiveAllConfirmationMessage": "Are you sure you want to archive all notifications? This operation cannot be undone.",
"StarDocument": "Star document",
"UnstarDocument": "Unstar document",
"Unsubscribe": "Unsubscribe"
"Unsubscribe": "Unsubscribe",
"Push": "Push"
}
}

View File

@ -42,6 +42,7 @@
"ArchiveAllConfirmationTitle": "¿Archivar todas las notificaciones?",
"ArchiveAllConfirmationMessage": "¿Estás seguro de que quieres archivar todas las notificaciones? Esta operación no se puede deshacer.",
"StarDocument": "Marcar documento",
"UnstarDocument": "Desmarcar documento"
"UnstarDocument": "Desmarcar documento",
"Push": "Push"
}
}

View File

@ -42,6 +42,7 @@
"ArchiveAllConfirmationTitle": "Arquivar todas as notificações?",
"ArchiveAllConfirmationMessage": "Esta seguro que quer arquivar todas as notificações? Esta operação não se pode desfazer.",
"StarDocument": "Marcar documento",
"UnstarDocument": "Desmarcar documento"
"UnstarDocument": "Desmarcar documento",
"Push": "Push"
}
}

View File

@ -42,6 +42,7 @@
"ArchiveAllConfirmationMessage": "Вы уверены, что хотите заархивировать все уведомления? Эту операцию невозможно отменить.",
"StarDocument": "Добавить в избранное",
"UnstarDocument": "Удалить из избранного",
"Unsubscribe": "Отписаться"
"Unsubscribe": "Отписаться",
"Push": "Push"
}
}

View File

@ -1,5 +1,5 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
// 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
@ -13,132 +13,60 @@
// limitations under the License.
-->
<script lang="ts">
import { PersonAccount } from '@hcengineering/contact'
import { getCurrentAccount, Ref } from '@hcengineering/core'
import {
NotificationProvider,
NotificationSetting,
NotificationStatus,
Notification as PlatformNotification,
BaseNotificationType
} from '@hcengineering/notification'
import { getCurrentAccount } from '@hcengineering/core'
import notification, { BrowserNotification, NotificationStatus } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getCurrentLocation, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view'
import notification from '../plugin'
import { getCurrentLocation, navigate } from '@hcengineering/ui'
import { askPermission } from '../utils'
let notifications: BrowserNotification[] = []
const query = createQuery()
const settingQuery = createQuery()
const providersQuery = createQuery()
let settingsReceived = false
let settings: Map<Ref<BaseNotificationType>, NotificationSetting> = new Map<
Ref<BaseNotificationType>,
NotificationSetting
>()
let provider: NotificationProvider | undefined
$: enabled = 'Notification' in window && Notification?.permission !== 'denied'
$: enabled &&
providersQuery.query(
notification.class.NotificationProvider,
{ _id: notification.providers.BrowserNotification },
(res) => {
provider = res[0]
}
)
$: enabled &&
settingQuery.query(notification.class.NotificationSetting, {}, (res) => {
settings = new Map(
res.map((setting) => {
return [setting.type, setting]
})
)
settingsReceived = true
})
const alreadyShown = new Set<Ref<PlatformNotification>>()
$: enabled &&
settingsReceived &&
provider !== undefined &&
query.query(
notification.class.Notification,
{
attachedTo: (getCurrentAccount() as PersonAccount).person,
status: { $nin: [NotificationStatus.Read] }
},
(res) => {
process(res.reverse())
},
{
sort: {
modifiedOn: 1
}
}
)
async function process (notifications: PlatformNotification[]): Promise<void> {
for (const notification of notifications) {
await tryNotify(notification)
query.query(
notification.class.BrowserNotification,
{
user: getCurrentAccount()._id,
status: NotificationStatus.New
},
(res) => {
notifications = res
}
}
)
async function tryNotify (notification: PlatformNotification): Promise<void> {
const text = notification.text.replace(/<[^>]*>/g, '').trim()
if (text === '') return
const setting = settings.get(notification.type)
const enabled = setting?.enabled
if (!enabled) return
if ((setting?.modifiedOn ?? notification.modifiedOn) < 0) return
if (Notification?.permission !== 'granted') {
await Notification?.requestPermission()
}
if (Notification?.permission === 'granted') {
await notify(text, notification)
}
}
const client = getClient()
const hierarchy = client.getHierarchy()
let clearTimer: any | undefined
$: process(notifications)
async function notify (text: string, notifyInstance: PlatformNotification): Promise<void> {
if (notifyInstance.status !== NotificationStatus.New) {
return
}
if (alreadyShown.has(notifyInstance._id)) {
return
}
alreadyShown.add(notifyInstance._id)
client.updateDoc(notifyInstance._class, notifyInstance.space, notifyInstance._id, {
status: NotificationStatus.Notified
})
if (clearTimer) {
clearTimeout(clearTimer)
}
clearTimer = setTimeout(() => {
alreadyShown.clear()
}, 5000)
// eslint-disable-next-line
const notification = new Notification(getCurrentLocation().path[1], {
tag: notifyInstance._id,
icon: '/favicon.png',
body: text
})
notification.onclick = () => {
const panelComponent = hierarchy.classHierarchyMixin(notifyInstance.attachedToClass, view.mixin.ObjectPanel)
const component = panelComponent?.component ?? view.component.EditDoc
showPanel(component, notifyInstance.attachedTo, notifyInstance.attachedToClass, 'content')
async function process (notifications: BrowserNotification[]): Promise<void> {
if (notifications.length === 0) return
await askPermission()
if ('Notification' in window && Notification?.permission === 'granted') {
for (const value of notifications) {
const req: NotificationOptions = {
body: value.body,
tag: value._id
}
const notification = new Notification(value.title, req)
if (value.onClickLocation !== undefined) {
const loc = getCurrentLocation()
loc.path.length = 3
loc.path[2] = value.onClickLocation.path[2]
if (value.onClickLocation.path[3]) {
loc.path[3] = value.onClickLocation.path[3]
if (value.onClickLocation.path[4]) {
loc.path[4] = value.onClickLocation.path[4]
}
}
loc.query = value.onClickLocation.query
loc.fragment = value.onClickLocation.fragment
const onClick = () => {
navigate(loc)
window.parent.parent.focus()
}
notification.onclick = onClick
}
await client.update(value, { status: NotificationStatus.Notified })
}
}
}
</script>

View File

@ -21,32 +21,20 @@
NotificationSetting,
NotificationType
} from '@hcengineering/notification'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { IntlString, getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Grid, Label, ToggleWithLabel } from '@hcengineering/ui'
import notification from '../plugin'
import { validateHeaderName } from 'http'
export let group: Ref<NotificationGroup>
export let settings: Map<Ref<BaseNotificationType>, NotificationSetting[]>
const client = getClient()
let types: BaseNotificationType[] = []
let typesMap: IdMap<BaseNotificationType> = new Map()
let providers: NotificationProvider[] = []
let providersMap: IdMap<NotificationProvider> = new Map()
void load()
const query = createQuery()
$: query.query(notification.class.BaseNotificationType, { group }, (res) => {
types = res
typesMap = toIdMap(types)
})
async function load () {
providers = await client.findAll(notification.class.NotificationProvider, {})
providersMap = toIdMap(providers)
}
$: 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
@ -64,22 +52,31 @@
return typeValue?.providers?.[provider] ?? false
}
function createHandler (type: Ref<BaseNotificationType>, provider: Ref<NotificationProvider>): (evt: any) => void {
function changeHandler (type: Ref<BaseNotificationType>, provider: Ref<NotificationProvider>): (evt: any) => void {
return (evt: any) => {
void change(type, provider, evt.detail)
}
}
async function change (
type: Ref<BaseNotificationType>,
provider: Ref<NotificationProvider>,
typeId: Ref<BaseNotificationType>,
providerId: Ref<NotificationProvider>,
value: boolean
): Promise<void> {
const current = getSetting(settings, type, provider)
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, notification.space.Notifications, {
attachedTo: provider,
type,
attachedTo: providerId,
type: typeId,
enabled: value
})
} else {
@ -87,6 +84,19 @@
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 (
@ -127,7 +137,7 @@
<ToggleWithLabel
label={provider.label}
on={getStatus(settings, type._id, provider._id)}
on:change={createHandler(type._id, provider._id)}
on:change={changeHandler(type._id, provider._id)}
/>
</div>
{:else}

View File

@ -44,12 +44,10 @@
const dispatch = createEventDispatcher()
const client = getClient()
let groups: NotificationGroup[] = []
let preferencesGroups: NotificationPreferencesGroup[] = []
void client.findAll(notification.class.NotificationGroup, {}).then((res) => {
groups = res
})
const groups: NotificationGroup[] = client.getModel().findAllSync(notification.class.NotificationGroup, {})
const preferencesGroups: NotificationPreferencesGroup[] = client
.getModel()
.findAllSync(notification.class.NotificationPreferencesGroup, {})
let settings = new Map<Ref<BaseNotificationType>, NotificationSetting[]>()
@ -68,10 +66,6 @@
let group: Ref<NotificationGroup> | undefined = undefined
let currentPreferenceGroup: NotificationPreferencesGroup | undefined = undefined
void client.findAll(notification.class.NotificationPreferencesGroup, {}).then((res) => {
preferencesGroups = res
})
$: if (!group && !currentPreferenceGroup) {
if (preferencesGroups.length > 0) {
currentPreferenceGroup = preferencesGroups[0]

View File

@ -15,15 +15,15 @@
import activity, { type ActivityMessage } from '@hcengineering/activity'
import {
SortingOrder,
generateId,
getCurrentAccount,
toIdMap,
type Class,
type Doc,
type IdMap,
type Ref,
type TxOperations,
type WithLookup,
generateId,
toIdMap,
type IdMap
type WithLookup
} from '@hcengineering/core'
import notification, {
type ActivityInboxNotification,

View File

@ -42,7 +42,8 @@ import {
hasInboxNotifications,
archiveAll,
readAll,
unreadAll
unreadAll,
checkPermission
} from './utils'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
@ -76,7 +77,8 @@ export default async (): Promise<Resources> => ({
HasDocNotifyContextUnpinAction: hasDocNotifyContextUnpinAction,
CanReadNotifyContext: canReadNotifyContext,
CanUnReadNotifyContext: canUnReadNotifyContext,
HasInboxNotifications: hasInboxNotifications
HasInboxNotifications: hasInboxNotifications,
CheckPushPermission: checkPermission
},
actionImpl: {
Unsubscribe: unsubscribe,

View File

@ -37,8 +37,8 @@ import notification, {
type DocNotifyContext,
type InboxNotification
} from '@hcengineering/notification'
import { getClient, MessageBox } from '@hcengineering/presentation'
import { getLocation, navigate, type Location, type ResolvedLocation, showPopup } from '@hcengineering/ui'
import { MessageBox, getClient } from '@hcengineering/presentation'
import { getLocation, navigate, showPopup, type Location, type ResolvedLocation } from '@hcengineering/ui'
import { get } from 'svelte/store'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
@ -511,3 +511,37 @@ export function openInboxDoc (
navigate(loc)
}
export async function checkPermission (value: boolean): Promise<boolean> {
if (!value) return true
if ('Notification' in window) {
if (Notification?.permission === 'denied') return false
if (Notification?.permission === 'granted') return true
if (Notification?.permission === 'default') {
const res = await Notification?.requestPermission()
return res === 'granted'
}
}
return false
}
export async function askPermission (): Promise<void> {
if ('Notification' in window && Notification?.permission === 'default') {
await Notification?.requestPermission()
}
}
export function notify (title: string, body: string, _id?: string, onClick?: () => void): void {
if ('Notification' in window && Notification?.permission === 'granted') {
const req: NotificationOptions = {
body
}
if (_id !== undefined) {
req.tag = _id
}
const notification = new Notification(title, req)
if (onClick !== undefined) {
notification.onclick = onClick
}
}
}

View File

@ -17,7 +17,6 @@ import { ActivityMessage } from '@hcengineering/activity'
import {
Account,
AnyAttribute,
AttachedDoc,
Class,
Doc,
DocumentQuery,
@ -43,11 +42,12 @@ export * from './types'
/**
* @public
*/
export interface Notification extends AttachedDoc {
tx: Ref<TxCUD<Doc>>
export interface BrowserNotification extends Doc {
user: Ref<Account>
status: NotificationStatus
text: string
type: Ref<NotificationType>
title: string
body: string
onClickLocation?: Location
}
/**
@ -55,8 +55,7 @@ export interface Notification extends AttachedDoc {
*/
export enum NotificationStatus {
New,
Notified,
Read
Notified
}
/**
@ -137,6 +136,8 @@ export interface CommonNotificationType extends BaseNotificationType {}
*/
export interface NotificationProvider extends Doc {
label: IntlString
depends?: Ref<NotificationProvider>
onChange?: Resource<(value: boolean) => Promise<boolean>>
}
/**
@ -312,6 +313,11 @@ export interface ActivityNotificationViewlet extends Doc {
presenter: AnyComponent
}
/**
* @public
*/
export type NotifyFunc = (title: string, body: string, _id?: string, onClick?: () => void) => void
/**
* @public
*/
@ -324,7 +330,7 @@ const notification = plugin(notificationId, {
NotificationContextPresenter: '' as Ref<Mixin<NotificationContextPresenter>>
},
class: {
Notification: '' as Ref<Class<Notification>>,
BrowserNotification: '' as Ref<Class<BrowserNotification>>,
BaseNotificationType: '' as Ref<Class<BaseNotificationType>>,
NotificationType: '' as Ref<Class<NotificationType>>,
CommonNotificationType: '' as Ref<Class<CommonNotificationType>>,
@ -403,9 +409,12 @@ const notification = plugin(notificationId, {
ArchiveAllConfirmationTitle: '' as IntlString,
ArchiveAllConfirmationMessage: '' as IntlString,
YouAddedCollaborators: '' as IntlString,
YouRemovedCollaborators: '' as IntlString
YouRemovedCollaborators: '' as IntlString,
Push: '' as IntlString
},
function: {
Notify: '' as Resource<NotifyFunc>,
CheckPushPermission: '' as Resource<(value: boolean) => Promise<boolean>>,
GetInboxNotificationsClient: '' as Resource<InboxNotificationsClientFactory>,
HasInboxNotifications: '' as Resource<
(notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>

View File

@ -128,7 +128,7 @@ export async function getPersonNotificationTxes (
notifyResult.allowed = false
}
const texes = await getCommonNotificationTxes(
const txes = await getCommonNotificationTxes(
control,
doc,
data,
@ -142,7 +142,7 @@ export async function getPersonNotificationTxes (
notification.class.MentionInboxNotification
)
res.push(...texes)
res.push(...txes)
return res
}

View File

@ -313,9 +313,17 @@ export async function OnDirectMessageSent (originTx: Tx, control: TriggerControl
if (anotherPerson == null) return []
await pushActivityInboxNotifications(dmCreationTx, control, res, anotherPerson, directChannel, notifyContexts, [
message
])
await pushActivityInboxNotifications(
dmCreationTx,
control,
res,
anotherPerson,
directChannel,
notifyContexts,
[message],
true,
true
)
} else if (notifyContext.hidden) {
res.push(
control.txFactory.createTxUpdateDoc(notifyContext._class, notifyContext.space, notifyContext._id, {

View File

@ -45,14 +45,17 @@ import core, {
import notification, {
ActivityInboxNotification,
BaseNotificationType,
BrowserNotification,
ClassCollaborators,
Collaborators,
CommonInboxNotification,
DocNotifyContext,
InboxNotification,
notificationId,
NotificationStatus,
NotificationType
} from '@hcengineering/notification'
import { getMetadata, getResource } from '@hcengineering/platform'
import { getMetadata, getResource, translate } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import serverNotification, {
getEmployee,
@ -135,7 +138,8 @@ export async function getCommonNotificationTxes (
notifyContexts,
data,
_class,
modifiedOn
modifiedOn,
notifyResult.push
)
}
@ -365,6 +369,7 @@ export async function pushInboxNotifications (
data: Partial<Data<InboxNotification>>,
_class: Ref<Class<InboxNotification>>,
modifiedOn: Timestamp,
shouldPush: boolean,
shouldUpdateTimestamp = true
): Promise<void> {
const context = getDocNotifyContext(contexts, targetUser, attachedTo, res)
@ -396,17 +401,118 @@ export async function pushInboxNotifications (
}
if (!isHidden) {
res.push(
control.txFactory.createTxCreateDoc(_class, space, {
user: targetUser,
isViewed: false,
docNotifyContext: docNotifyContextId,
...data
})
)
const notificationData = {
user: targetUser,
isViewed: false,
docNotifyContext: docNotifyContextId,
...data
}
const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData)
res.push(notificationTx)
if (shouldPush) {
const pushTx = await createPushFromInbox(control, targetUser, space, docNotifyContextId, notificationData, _class)
if (pushTx !== undefined) {
res.push(pushTx)
}
}
}
}
async function activityInboxNotificationToText (doc: Data<ActivityInboxNotification>): Promise<[string, string]> {
let title: string = ''
let body: string = ''
const params = doc.intlParams ?? {}
if (doc.intlParamsNotLocalized != null && Object.keys(doc.intlParamsNotLocalized).length > 0) {
for (const key in doc.intlParamsNotLocalized) {
const val = doc.intlParamsNotLocalized[key]
params[key] = await translate(val, params)
}
}
if (doc.title != null) {
title = await translate(doc.title, params)
}
if (doc.body != null) {
body = await translate(doc.body, params)
}
return [title, body]
}
async function commonInboxNotificationToText (doc: Data<CommonInboxNotification>): Promise<[string, string]> {
let title: string = ''
let body: string = ''
let params = doc.intlParams ?? {}
if (doc.props != null) {
params = { ...params, ...doc.props }
}
if (doc.intlParamsNotLocalized != null && Object.keys(doc.intlParamsNotLocalized).length > 0) {
for (const key in doc.intlParamsNotLocalized) {
const val = doc.intlParamsNotLocalized[key]
params[key] = await translate(val, params)
}
}
if (doc.header != null) {
title = await translate(doc.header, params)
}
if (doc.messageHtml != null) {
body = doc.messageHtml
}
if (doc.message != null) {
body = await translate(doc.message, params)
}
return [title, body]
}
export async function createPushFromInbox (
control: TriggerControl,
targetUser: Ref<Account>,
space: Ref<Space>,
docNotifyContextId: Ref<DocNotifyContext>,
data: Data<InboxNotification>,
_class: Ref<Class<InboxNotification>>
): 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.CommonInboxNotification)) {
;[title, body] = await commonInboxNotificationToText(data as Data<CommonInboxNotification>)
}
if (title === '' || body === '') {
return
}
return await createPushNotification(control, targetUser, space, title, body, [
'',
'',
notificationId,
docNotifyContextId
])
}
export async function createPushNotification (
control: TriggerControl,
targetUser: Ref<Account>,
space: Ref<Space>,
title: string,
body: string,
onClick?: string[]
): Promise<TxCreateDoc<BrowserNotification>> {
const data: Data<BrowserNotification> = {
user: targetUser,
status: NotificationStatus.New,
title,
body
}
if (onClick !== undefined) {
data.onClickLocation = {
path: onClick
}
}
const res = control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, space, data)
return res
}
/**
* @public
*/
@ -418,7 +524,8 @@ export async function pushActivityInboxNotifications (
object: Doc,
docNotifyContexts: DocNotifyContext[],
activityMessages: ActivityMessage[],
shouldUpdateTimestamp = true
shouldUpdateTimestamp: boolean,
shouldPush: boolean
): Promise<void> {
for (const activityMessage of activityMessages) {
const existNotifications = await control.findAll(notification.class.ActivityInboxNotification, {
@ -448,6 +555,7 @@ export async function pushActivityInboxNotifications (
data,
notification.class.ActivityInboxNotification,
activityMessage.modifiedOn,
shouldPush,
shouldUpdateTimestamp
)
}
@ -477,7 +585,8 @@ export async function getNotificationTxes (
object,
docNotifyContexts,
activityMessages,
shouldUpdateTimestamp
shouldUpdateTimestamp,
notifyResult.push
)
}
@ -699,7 +808,9 @@ async function updateCollaboratorsMixin (
collab,
prevDoc,
docNotifyContexts,
activityMessages
activityMessages,
true,
true
)
}
}

View File

@ -28,5 +28,6 @@ export interface Content {
*/
export interface NotifyResult {
allowed: boolean
push: boolean
emails: BaseNotificationType[]
}

View File

@ -114,19 +114,23 @@ export async function shouldNotifyCommon (
const emailTypes: BaseNotificationType[] = []
let allowed = false
let push = false
if (type === undefined) {
return { allowed, emails: emailTypes }
return { allowed, emails: emailTypes, push }
}
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)
}
return { allowed, emails: emailTypes }
return { allowed, push, emails: emailTypes }
}
export async function isAllowed (
@ -169,6 +173,7 @@ export async function isShouldNotifyTx (
docUpdateMessage?: DocUpdateMessage
): Promise<NotifyResult> {
let allowed = false
let push = false
const emailTypes: NotificationType[] = []
const types = await getMatchedTypes(
@ -203,12 +208,16 @@ export async function isShouldNotifyTx (
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)
}
}
return {
allowed,
push,
emails: emailTypes
}
}