mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
Browser notifications (#5261)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
8b0627fc0f
commit
10f59e1028
@ -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
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
},
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -42,6 +42,7 @@
|
||||
"ArchiveAllConfirmationMessage": "Вы уверены, что хотите заархивировать все уведомления? Эту операцию невозможно отменить.",
|
||||
"StarDocument": "Добавить в избранное",
|
||||
"UnstarDocument": "Удалить из избранного",
|
||||
"Unsubscribe": "Отписаться"
|
||||
"Unsubscribe": "Отписаться",
|
||||
"Push": "Push"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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, {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -28,5 +28,6 @@ export interface Content {
|
||||
*/
|
||||
export interface NotifyResult {
|
||||
allowed: boolean
|
||||
push: boolean
|
||||
emails: BaseNotificationType[]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user