mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-07 02:07:43 +02:00
Implement @everyone/@here (#8890)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -63,10 +63,9 @@ import {
|
||||
DOMAIN_DOC_NOTIFY,
|
||||
type ActivityInboxNotification,
|
||||
type ActivityNotificationViewlet,
|
||||
type BaseNotificationType,
|
||||
type NotificationType,
|
||||
type BrowserNotification,
|
||||
type CommonInboxNotification,
|
||||
type CommonNotificationType,
|
||||
type DocNotifyContext,
|
||||
type InboxNotification,
|
||||
type MentionInboxNotification,
|
||||
@@ -79,7 +78,6 @@ import {
|
||||
type NotificationProviderDefaults,
|
||||
type NotificationProviderSetting,
|
||||
type NotificationTemplate,
|
||||
type NotificationType,
|
||||
type NotificationTypeSetting,
|
||||
type PushSubscription,
|
||||
type PushSubscriptionKeys
|
||||
@@ -116,26 +114,19 @@ export class TPushSubscription extends TDoc implements PushSubscription {
|
||||
keys!: PushSubscriptionKeys
|
||||
}
|
||||
|
||||
@Model(notification.class.BaseNotificationType, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TBaseNotificationType extends TDoc implements BaseNotificationType {
|
||||
@Model(notification.class.NotificationType, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TNotificationType extends TDoc implements NotificationType {
|
||||
generated!: boolean
|
||||
label!: IntlString
|
||||
group!: Ref<NotificationGroup>
|
||||
defaultEnabled!: boolean
|
||||
hidden!: boolean
|
||||
templates?: NotificationTemplate
|
||||
}
|
||||
|
||||
@Model(notification.class.NotificationType, notification.class.BaseNotificationType)
|
||||
export class TNotificationType extends TBaseNotificationType implements NotificationType {
|
||||
txClasses!: Ref<Class<Tx>>[]
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
onlyOwn?: boolean
|
||||
}
|
||||
|
||||
@Model(notification.class.CommonNotificationType, notification.class.BaseNotificationType)
|
||||
export class TCommonNotificationType extends TBaseNotificationType implements CommonNotificationType {}
|
||||
|
||||
@Model(notification.class.NotificationGroup, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TNotificationGroup extends TDoc implements NotificationGroup {
|
||||
label!: IntlString
|
||||
@@ -154,7 +145,7 @@ export class TNotificationPreferencesGroup extends TDoc implements NotificationP
|
||||
@Model(notification.class.NotificationTypeSetting, preference.class.Preference)
|
||||
export class TNotificationTypeSetting extends TPreference implements NotificationTypeSetting {
|
||||
declare attachedTo: Ref<TNotificationProvider>
|
||||
type!: Ref<BaseNotificationType>
|
||||
type!: Ref<NotificationType>
|
||||
enabled!: boolean
|
||||
}
|
||||
|
||||
@@ -247,6 +238,8 @@ export class TInboxNotification extends TDoc implements InboxNotification {
|
||||
|
||||
declare space: Ref<PersonSpace>
|
||||
|
||||
types?: Ref<NotificationType>[]
|
||||
|
||||
title?: IntlString
|
||||
body?: IntlString
|
||||
intlParams?: Record<string, string | number>
|
||||
@@ -319,9 +312,9 @@ export class TNotificationProvider extends TDoc implements NotificationProvider
|
||||
@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>[]
|
||||
excludeIgnore?: Ref<NotificationType>[]
|
||||
ignoredTypes!: Ref<NotificationType>[]
|
||||
enabledTypes!: Ref<NotificationType>[]
|
||||
}
|
||||
|
||||
export const notificationActionTemplates = template({
|
||||
@@ -363,8 +356,7 @@ export function createModel (builder: Builder): void {
|
||||
TCommonInboxNotification,
|
||||
TNotificationContextPresenter,
|
||||
TActivityNotificationViewlet,
|
||||
TBaseNotificationType,
|
||||
TCommonNotificationType,
|
||||
TNotificationType,
|
||||
TMentionInboxNotification,
|
||||
TPushSubscription,
|
||||
TNotificationProvider,
|
||||
@@ -558,15 +550,20 @@ export function createModel (builder: Builder): void {
|
||||
builder.mixin(notification.class.CommonInboxNotification, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: notification.component.CommonInboxNotificationPresenter
|
||||
})
|
||||
builder.mixin(notification.class.MentionInboxNotification, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: notification.component.MentionInboxNotificationPresenter
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.CommonNotificationType,
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
{
|
||||
label: activity.string.Mentions,
|
||||
generated: false,
|
||||
hidden: false,
|
||||
group: notification.ids.NotificationGroup,
|
||||
txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc],
|
||||
objectClass: core.class.Doc,
|
||||
defaultEnabled: true,
|
||||
templates: {
|
||||
textTemplate: '{sender} mentioned you in {doc}: {message}',
|
||||
@@ -574,9 +571,8 @@ export function createModel (builder: Builder): void {
|
||||
subjectTemplate: 'You were mentioned in {doc}'
|
||||
}
|
||||
},
|
||||
notification.ids.MentionCommonNotificationType
|
||||
notification.ids.MentionNotificationType
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
@@ -808,7 +804,7 @@ 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>[] = []
|
||||
const enabledInboxTypes: Ref<NotificationType>[] = []
|
||||
|
||||
for (const attribute of filtered) {
|
||||
if (ignoreKeys.includes(attribute.name)) continue
|
||||
|
||||
@@ -47,7 +47,8 @@ export default mergeIds(notificationId, notification, {
|
||||
component: {
|
||||
NotificationSettings: '' as AnyComponent,
|
||||
ActivityInboxNotificationPresenter: '' as AnyComponent,
|
||||
CommonInboxNotificationPresenter: '' as AnyComponent
|
||||
CommonInboxNotificationPresenter: '' as AnyComponent,
|
||||
MentionInboxNotificationPresenter: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
HasDocNotifyContextPinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
|
||||
@@ -59,7 +59,8 @@ export function createModel (builder: Builder): void {
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverActivity.trigger.OnDocRemoved
|
||||
trigger: serverActivity.trigger.OnDocRemoved,
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
|
||||
@@ -80,7 +80,8 @@ export function createModel (builder: Builder): void {
|
||||
txMatch: {
|
||||
_class: core.class.TxRemoveDoc,
|
||||
objectClass: chunter.class.ChatMessage
|
||||
}
|
||||
},
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
|
||||
@@ -98,4 +98,13 @@ export function createModel (builder: Builder): void {
|
||||
objectClass: notification.class.InboxNotification
|
||||
}
|
||||
})
|
||||
|
||||
builder.mixin(
|
||||
notification.ids.MentionNotificationType,
|
||||
notification.class.NotificationType,
|
||||
serverNotification.mixin.TypeMatch,
|
||||
{
|
||||
func: serverNotification.function.MentionTypeMatch
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -244,6 +244,8 @@ export interface SearchResultDoc {
|
||||
shortTitleComponent?: SearchComponentWithProps
|
||||
title?: string
|
||||
titleComponent?: SearchComponentWithProps
|
||||
description?: string
|
||||
emojiIcon?: string
|
||||
score?: number
|
||||
doc: Pick<Doc, '_id' | '_class'>
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"dependencies": {
|
||||
"@hcengineering/analytics": "^0.6.0",
|
||||
"@hcengineering/client": "^0.6.18",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/collaborator-client": "^0.6.4",
|
||||
"@hcengineering/communication-client-query": "0.1.180",
|
||||
"@hcengineering/communication-sdk-types": "0.1.180",
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { SearchResultDoc } from '@hcengineering/core'
|
||||
import { notEmpty, SearchResultDoc } from '@hcengineering/core'
|
||||
import { getResourceC } from '@hcengineering/platform'
|
||||
import { Icon, type AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { Icon, type AnySvelteComponent, IconWithEmoji } from '@hcengineering/ui'
|
||||
|
||||
export let value: SearchResultDoc
|
||||
|
||||
@@ -45,6 +45,15 @@
|
||||
<div class="icon-place">
|
||||
<svelte:component this={iconComponent} size={'smaller'} {...value.iconComponent?.props} />
|
||||
</div>
|
||||
{:else if value.emojiIcon}
|
||||
<div class="emoji">
|
||||
<IconWithEmoji
|
||||
icon={Array.from(value.emojiIcon)
|
||||
.map((c) => c.codePointAt(0))
|
||||
.filter(notEmpty)}
|
||||
size={'small'}
|
||||
/>
|
||||
</div>
|
||||
{:else if icon !== undefined}
|
||||
<Icon {icon} size={'small'} />
|
||||
{/if}
|
||||
@@ -60,6 +69,10 @@
|
||||
{:else}
|
||||
<span class="name">{value.title}</span>
|
||||
{/if}
|
||||
|
||||
{#if value.description !== undefined}
|
||||
<span class="description">{value.description}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +80,14 @@
|
||||
.icon-place {
|
||||
width: 1.75rem;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
.searchResult {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -80,5 +101,10 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding-left: 0.5rem;
|
||||
color: var(--global-secondary-TextColor);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { Component, Icon, showPopup } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
import { createQuery, getClient } from '../../utils'
|
||||
import MessageBox from '../MessageBox.svelte'
|
||||
@@ -31,10 +32,14 @@
|
||||
|
||||
let doc: Doc | undefined = undefined
|
||||
let broken: boolean = false
|
||||
const withoutDoc = [contact.mention.Here, contact.mention.Everyone]
|
||||
|
||||
$: icon = _class !== undefined && hierarchy.hasClass(_class) ? hierarchy.getClass(_class).icon : null
|
||||
$: icon =
|
||||
_class !== undefined && hierarchy.hasClass(_class) && !withoutDoc.includes(_id as any)
|
||||
? hierarchy.getClass(_class).icon
|
||||
: null
|
||||
|
||||
$: if (_class != null && _id != null && hierarchy.hasClass(_class)) {
|
||||
$: if (_class != null && _id != null && hierarchy.hasClass(_class) && !withoutDoc.includes(_id as any)) {
|
||||
docQuery.query(_class, { _id }, (r) => {
|
||||
doc = r.shift()
|
||||
broken = doc === undefined
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
export let objectId: Ref<Doc>
|
||||
export let space: Ref<Space>
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let docClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let content: Markup = EmptyMarkup
|
||||
export let iconSend: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let labelSend: IntlString | undefined = undefined
|
||||
@@ -441,6 +442,7 @@
|
||||
autofocus={autofocus ? 'end' : false}
|
||||
loading={loading || progress}
|
||||
{boundary}
|
||||
{docClass}
|
||||
canEmbedFiles={false}
|
||||
canEmbedImages={false}
|
||||
extraActions={[
|
||||
|
||||
@@ -230,6 +230,7 @@
|
||||
{focusIndex}
|
||||
bind:this={inputRef}
|
||||
bind:content={inputContent}
|
||||
docClass={object._class}
|
||||
{_class}
|
||||
space={getChannelSpace(object._class, object._id, object.space)}
|
||||
skipAttachmentsPreload={(currentMessage.attachments ?? 0) === 0}
|
||||
|
||||
@@ -111,6 +111,10 @@
|
||||
"ViberPlaceholder": "Viber",
|
||||
"UserProfile": "Uživatelský profil",
|
||||
"DeactivatedAccount": "Deaktivovaný účet",
|
||||
"LocalTime": "místní čas"
|
||||
"LocalTime": "místní čas",
|
||||
"Everyone": "Všichni",
|
||||
"Here": "Zde",
|
||||
"EveryoneDescription": "Upozornit všechny v tomto {title}",
|
||||
"HereDescription": "Upozornit všechny v tomto {title}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,10 @@
|
||||
"ViberPlaceholder": "Viber",
|
||||
"UserProfile": "Benutzerprofil",
|
||||
"DeactivatedAccount": "Deaktivierter Account",
|
||||
"LocalTime": "Ortszeit"
|
||||
"LocalTime": "Ortszeit",
|
||||
"Everyone": "Jeder",
|
||||
"Here": "Hier",
|
||||
"EveryoneDescription": "Benachrichtigen Sie alle in diesem {title}",
|
||||
"HereDescription": "Benachrichtigen Sie alle in diesem {title}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
"Confirmed": "Confirmed",
|
||||
"UserProfile": "User profile",
|
||||
"DeactivatedAccount": "Deactivated account",
|
||||
"LocalTime": "local time"
|
||||
"LocalTime": "local time",
|
||||
"Everyone": "Everyone",
|
||||
"Here": "Here",
|
||||
"EveryoneDescription": "Notify everyone in this {title}",
|
||||
"HereDescription": "Notify every online member in this {title}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
"Confirmed": "Confirmado",
|
||||
"UserProfile": "Perfil de usuario",
|
||||
"DeactivatedAccount": "Cuenta desactivada",
|
||||
"LocalTime": "hora local"
|
||||
"LocalTime": "hora local",
|
||||
"Everyone": "Todos",
|
||||
"Here": "Aquí",
|
||||
"EveryoneDescription": "Notificar a todos en este {title}",
|
||||
"HereDescription": "Notificar a todos en este {title}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
"Confirmed": "Confirmé",
|
||||
"UserProfile": "Profil utilisateur",
|
||||
"DeactivatedAccount": "Compte désactivé",
|
||||
"LocalTime": "heure locale"
|
||||
"LocalTime": "heure locale",
|
||||
"Everyone": "Tout le monde",
|
||||
"Here": "Ici",
|
||||
"EveryoneDescription": "Notifier tout le monde dans ce {title}",
|
||||
"HereDescription": "Notifier tout le monde dans ce {title}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
"Confirmed": "Confermato",
|
||||
"UserProfile": "Profilo utente",
|
||||
"DeactivatedAccount": "Account disattivato",
|
||||
"LocalTime": "ora locale"
|
||||
"LocalTime": "ora locale",
|
||||
"Everyone": "Tutti",
|
||||
"Here": "Qui",
|
||||
"EveryoneDescription": "Notifica tutti in questo {title}",
|
||||
"HereDescription": "Notifica tutti in questo {title}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
"Confirmed": "確認済み",
|
||||
"UserProfile": "ユーザープロフィール",
|
||||
"DeactivatedAccount": "アカウントは無効化されました",
|
||||
"LocalTime": "現地時間"
|
||||
"LocalTime": "現地時間",
|
||||
"Everyone": "全員",
|
||||
"Here": "ここ",
|
||||
"EveryoneDescription": "この{title}のすべてに通知する",
|
||||
"HereDescription": "この{title}のすべてに通知する"
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,10 @@
|
||||
"Confirmed": "Confirmado",
|
||||
"UserProfile": "Perfil do usuário",
|
||||
"DeactivatedAccount": "Conta desativada",
|
||||
"LocalTime": "hora local"
|
||||
"LocalTime": "hora local",
|
||||
"Everyone": "Todos",
|
||||
"Here": "Aqui",
|
||||
"EveryoneDescription": "Notificar todos neste {title}",
|
||||
"HereDescription": "Notificar todos neste {title}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
"Confirmed": "Подтвержден",
|
||||
"UserProfile": "Профиль пользователя",
|
||||
"DeactivatedAccount": "Деактивированный аккаунт",
|
||||
"LocalTime": "местного времени"
|
||||
"LocalTime": "местного времени",
|
||||
"Everyone": "Все",
|
||||
"Here": "Здесь",
|
||||
"EveryoneDescription": "Уведомить всех в этом {title}",
|
||||
"HereDescription": "Уведомить всех в этом{title}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
"Confirmed": "已确认",
|
||||
"UserProfile": "用户资料",
|
||||
"DeactivatedAccount": "已停用账户",
|
||||
"LocalTime": "当地时间"
|
||||
"LocalTime": "当地时间",
|
||||
"Everyone": "所有人",
|
||||
"Here": "这里",
|
||||
"EveryoneDescription": "通知所有人在此{title}",
|
||||
"HereDescription": "通知所有人在此{title}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,11 @@ export const contactPlugin = plugin(contactId, {
|
||||
Confirmed: '' as IntlString,
|
||||
UserProfile: '' as IntlString,
|
||||
DeactivatedAccount: '' as IntlString,
|
||||
LocalTime: '' as IntlString
|
||||
LocalTime: '' as IntlString,
|
||||
Everyone: '' as IntlString,
|
||||
Here: '' as IntlString,
|
||||
EveryoneDescription: '' as IntlString,
|
||||
HereDescription: '' as IntlString
|
||||
},
|
||||
viewlet: {
|
||||
TableMember: '' as Ref<Viewlet>,
|
||||
@@ -375,6 +379,10 @@ export const contactPlugin = plugin(contactId, {
|
||||
ids: {
|
||||
MentionCommonNotificationType: '' as Ref<Doc>
|
||||
},
|
||||
mention: {
|
||||
Everyone: '' as Ref<Employee>,
|
||||
Here: '' as Ref<Employee>
|
||||
},
|
||||
extension: {
|
||||
EmployeePopupActions: '' as ComponentExtensionId,
|
||||
PersonAchievementsPresenter: '' as ComponentExtensionId
|
||||
|
||||
@@ -61,11 +61,12 @@
|
||||
function updateDisplayData (data: InboxData): void {
|
||||
let result: [Ref<DocNotifyContext>, DisplayInboxNotification[]][] = Array.from(data.entries())
|
||||
if (archivedContexts.size > 0) {
|
||||
result = result.filter(([contextId]) => {
|
||||
result = result.filter(([contextId, d]) => {
|
||||
const context = $contextByIdStore.get(contextId)
|
||||
return (
|
||||
!archivedContexts.has(contextId) ||
|
||||
(context?.lastUpdateTimestamp ?? 0) > (archivedContexts.get(contextId) ?? 0)
|
||||
(context?.lastUpdateTimestamp ?? 0) > (archivedContexts.get(contextId) ?? 0) ||
|
||||
(d[0]?.createdOn ?? 0) > (archivedContexts.get(contextId) ?? 0)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<!--
|
||||
// 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 { ActivityMessagePreview } from '@hcengineering/activity-resources'
|
||||
import { MentionInboxNotification } from '@hcengineering/notification'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import { Doc, Ref, Space } from '@hcengineering/core'
|
||||
|
||||
import CommonInboxNotificationPresenter from './CommonInboxNotificationPresenter.svelte'
|
||||
|
||||
export let object: Doc | undefined
|
||||
export let value: MentionInboxNotification
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const query = createQuery()
|
||||
|
||||
let message: ActivityMessage | undefined = undefined
|
||||
|
||||
$: if (hierarchy.isDerived(value.mentionedInClass, activity.class.ActivityMessage)) {
|
||||
query.query(value.mentionedInClass, { _id: value.mentionedIn }, (res) => {
|
||||
message = res[0] as ActivityMessage
|
||||
})
|
||||
} else {
|
||||
query.unsubscribe()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if message}
|
||||
<ActivityMessagePreview value={message} {space} doc={object} on:click />
|
||||
{:else}
|
||||
<CommonInboxNotificationPresenter {value} on:click />
|
||||
{/if}
|
||||
+12
-13
@@ -15,10 +15,9 @@
|
||||
<script lang="ts">
|
||||
import core, { IdMap, Ref, toIdMap } from '@hcengineering/core'
|
||||
import {
|
||||
BaseNotificationType,
|
||||
NotificationType,
|
||||
NotificationProvider,
|
||||
NotificationGroup,
|
||||
NotificationType,
|
||||
NotificationTypeSetting,
|
||||
NotificationProviderDefaults,
|
||||
NotificationProviderSetting
|
||||
@@ -31,7 +30,7 @@
|
||||
import { providersSettings } from '../../utils'
|
||||
|
||||
export let group: Ref<NotificationGroup>
|
||||
export let settings: Map<Ref<BaseNotificationType>, NotificationTypeSetting[]>
|
||||
export let settings: Map<Ref<NotificationType>, NotificationTypeSetting[]>
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@@ -44,12 +43,12 @@
|
||||
.findAllSync(notification.class.NotificationProviderDefaults, {})
|
||||
const providersMap: IdMap<NotificationProvider> = toIdMap(providers)
|
||||
|
||||
$: types = client.getModel().findAllSync(notification.class.BaseNotificationType, { group })
|
||||
$: types = client.getModel().findAllSync(notification.class.NotificationType, { group })
|
||||
$: typesMap = toIdMap(types)
|
||||
|
||||
function getStatus (
|
||||
settings: Map<Ref<BaseNotificationType>, NotificationTypeSetting[]>,
|
||||
type: Ref<BaseNotificationType>,
|
||||
settings: Map<Ref<NotificationType>, NotificationTypeSetting[]>,
|
||||
type: Ref<NotificationType>,
|
||||
provider: Ref<NotificationProvider>
|
||||
): boolean {
|
||||
const setting = getTypeSetting(settings, type, provider)
|
||||
@@ -64,7 +63,7 @@
|
||||
}
|
||||
|
||||
async function onToggle (
|
||||
typeId: Ref<BaseNotificationType>,
|
||||
typeId: Ref<NotificationType>,
|
||||
providerId: Ref<NotificationProvider>,
|
||||
value: boolean
|
||||
): Promise<void> {
|
||||
@@ -96,8 +95,8 @@
|
||||
}
|
||||
|
||||
function getTypeSetting (
|
||||
map: Map<Ref<BaseNotificationType>, NotificationTypeSetting[]>,
|
||||
type: Ref<BaseNotificationType>,
|
||||
map: Map<Ref<NotificationType>, NotificationTypeSetting[]>,
|
||||
type: Ref<NotificationType>,
|
||||
provider: Ref<NotificationProvider>
|
||||
): NotificationTypeSetting | undefined {
|
||||
const typeMap = map.get(type)
|
||||
@@ -105,11 +104,11 @@
|
||||
return typeMap.find((p) => p.attachedTo === provider)
|
||||
}
|
||||
|
||||
const isNotificationType = (type: BaseNotificationType): type is NotificationType => {
|
||||
const isNotificationType = (type: NotificationType): type is NotificationType => {
|
||||
return type._class === notification.class.NotificationType
|
||||
}
|
||||
|
||||
function getLabel (type: BaseNotificationType): IntlString {
|
||||
function getLabel (type: NotificationType): IntlString {
|
||||
if (isNotificationType(type) && type.attachedToClass !== undefined) {
|
||||
return notification.string.AddedRemoved
|
||||
}
|
||||
@@ -117,7 +116,7 @@
|
||||
return notification.string.Change
|
||||
}
|
||||
|
||||
function isIgnored (type: Ref<BaseNotificationType>, provider: NotificationProvider): boolean {
|
||||
function isIgnored (type: Ref<NotificationType>, provider: NotificationProvider): boolean {
|
||||
const ignored = providerDefaults.some((it) => provider._id === it.provider && it.ignoredTypes.includes(type))
|
||||
|
||||
if (ignored) return true
|
||||
@@ -133,7 +132,7 @@
|
||||
|
||||
async function getFilteredProviders (
|
||||
providers: NotificationProvider[],
|
||||
types: BaseNotificationType[],
|
||||
types: NotificationType[],
|
||||
providersSettings: NotificationProviderSetting[]
|
||||
): Promise<NotificationProvider[]> {
|
||||
const result: NotificationProvider[] = []
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { onDestroy } from 'svelte'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import type {
|
||||
BaseNotificationType,
|
||||
NotificationType,
|
||||
NotificationGroup,
|
||||
NotificationPreferencesGroup,
|
||||
NotificationTypeSetting
|
||||
@@ -48,7 +48,7 @@
|
||||
.getModel()
|
||||
.findAllSync(notification.class.NotificationPreferencesGroup, {})
|
||||
|
||||
let settings = new Map<Ref<BaseNotificationType>, NotificationTypeSetting[]>()
|
||||
let settings = new Map<Ref<NotificationType>, NotificationTypeSetting[]>()
|
||||
|
||||
let isProviderSettingLoading = true
|
||||
let isTypeSettingLoading = true
|
||||
|
||||
@@ -23,6 +23,7 @@ import DocNotifyContextPresenter from './components/DocNotifyContextPresenter.sv
|
||||
import CollaboratorsChanged from './components/activity/CollaboratorsChanged.svelte'
|
||||
import ActivityInboxNotificationPresenter from './components/inbox/ActivityInboxNotificationPresenter.svelte'
|
||||
import CommonInboxNotificationPresenter from './components/inbox/CommonInboxNotificationPresenter.svelte'
|
||||
import MentionInboxNotificationPresenter from './components/inbox/MentionInboxNotificationPresenter.svelte'
|
||||
import NotificationCollaboratorsChanged from './components/NotificationCollaboratorsChanged.svelte'
|
||||
import ReactionNotificationPresenter from './components/ReactionNotificationPresenter.svelte'
|
||||
import GeneralPreferencesGroup from './components/settings/GeneralPreferencesGroup.svelte'
|
||||
@@ -65,6 +66,7 @@ export default async (): Promise<Resources> => ({
|
||||
DocNotifyContextPresenter,
|
||||
ActivityInboxNotificationPresenter,
|
||||
CommonInboxNotificationPresenter,
|
||||
MentionInboxNotificationPresenter,
|
||||
NotificationCollaboratorsChanged,
|
||||
ReactionNotificationPresenter,
|
||||
GeneralPreferencesGroup
|
||||
|
||||
@@ -40,7 +40,7 @@ import core, {
|
||||
import notification, {
|
||||
notificationId,
|
||||
type ActivityInboxNotification,
|
||||
type BaseNotificationType,
|
||||
type NotificationType,
|
||||
type Collaborators,
|
||||
type DisplayInboxNotification,
|
||||
type DocNotifyContext,
|
||||
@@ -821,7 +821,7 @@ export function notificationsComparator (notifications1: InboxNotification, noti
|
||||
return 0
|
||||
}
|
||||
|
||||
export function isNotificationAllowed (type: BaseNotificationType, providerId: Ref<NotificationProvider>): boolean {
|
||||
export function isNotificationAllowed (type: NotificationType, providerId: Ref<NotificationProvider>): boolean {
|
||||
const client = getClient()
|
||||
const provider = client.getModel().findAllSync(notification.class.NotificationProvider, { _id: providerId })[0]
|
||||
if (provider === undefined) return false
|
||||
|
||||
@@ -124,7 +124,10 @@ export interface NotificationContent {
|
||||
intlParamsNotLocalized?: Record<string, IntlString>
|
||||
}
|
||||
|
||||
export interface BaseNotificationType extends Doc {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface NotificationType extends Doc {
|
||||
label: IntlString
|
||||
// Is autogenerated
|
||||
generated: boolean
|
||||
@@ -134,11 +137,6 @@ export interface BaseNotificationType extends Doc {
|
||||
defaultEnabled: boolean
|
||||
// templates for email (and browser/push?)
|
||||
templates?: NotificationTemplate
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface NotificationType extends BaseNotificationType {
|
||||
// For show/hide with attributes
|
||||
attribute?: Ref<AnyAttribute>
|
||||
txClasses: Ref<Class<Tx>>[]
|
||||
@@ -156,8 +154,6 @@ export interface NotificationType extends BaseNotificationType {
|
||||
allowedForAuthor?: boolean
|
||||
}
|
||||
|
||||
export interface CommonNotificationType extends BaseNotificationType {}
|
||||
|
||||
export interface NotificationProvider extends Doc {
|
||||
label: IntlString
|
||||
description: IntlString
|
||||
@@ -173,9 +169,9 @@ export interface NotificationProvider extends Doc {
|
||||
|
||||
export interface NotificationProviderDefaults extends Doc {
|
||||
provider: Ref<NotificationProvider>
|
||||
excludeIgnore?: Ref<BaseNotificationType>[]
|
||||
ignoredTypes: Ref<BaseNotificationType>[]
|
||||
enabledTypes: Ref<BaseNotificationType>[]
|
||||
excludeIgnore?: Ref<NotificationType>[]
|
||||
ignoredTypes: Ref<NotificationType>[]
|
||||
enabledTypes: Ref<NotificationType>[]
|
||||
}
|
||||
|
||||
export interface NotificationProviderSetting extends Preference {
|
||||
@@ -185,7 +181,7 @@ export interface NotificationProviderSetting extends Preference {
|
||||
|
||||
export interface NotificationTypeSetting extends Preference {
|
||||
attachedTo: Ref<NotificationProvider>
|
||||
type: Ref<BaseNotificationType>
|
||||
type: Ref<NotificationType>
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
@@ -239,6 +235,7 @@ export interface InboxNotification extends Doc<PersonSpace> {
|
||||
docNotifyContext: Ref<DocNotifyContext>
|
||||
objectId: Ref<Doc>
|
||||
objectClass: Ref<Class<Doc>>
|
||||
types?: Ref<NotificationType>[]
|
||||
|
||||
// For browser notifications
|
||||
title?: IntlString
|
||||
@@ -351,9 +348,7 @@ const notification = plugin(notificationId, {
|
||||
class: {
|
||||
BrowserNotification: '' as Ref<Class<BrowserNotification>>,
|
||||
PushSubscription: '' as Ref<Class<PushSubscription>>,
|
||||
BaseNotificationType: '' as Ref<Class<BaseNotificationType>>,
|
||||
NotificationType: '' as Ref<Class<NotificationType>>,
|
||||
CommonNotificationType: '' as Ref<Class<CommonNotificationType>>,
|
||||
NotificationGroup: '' as Ref<Class<NotificationGroup>>,
|
||||
NotificationPreferencesGroup: '' as Ref<Class<NotificationPreferencesGroup>>,
|
||||
DocNotifyContext: '' as Ref<Class<DocNotifyContext>>,
|
||||
@@ -371,7 +366,7 @@ const notification = plugin(notificationId, {
|
||||
NotificationSettings: '' as Ref<Doc>,
|
||||
NotificationGroup: '' as Ref<NotificationGroup>,
|
||||
CollaboratoAddNotification: '' as Ref<NotificationType>,
|
||||
MentionCommonNotificationType: '' as Ref<CommonNotificationType>
|
||||
MentionNotificationType: '' as Ref<NotificationType>
|
||||
},
|
||||
metadata: {
|
||||
PushPublicKey: '' as Metadata<string>
|
||||
@@ -449,9 +444,7 @@ const notification = plugin(notificationId, {
|
||||
HasInboxNotifications: '' as Resource<
|
||||
(notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>
|
||||
>,
|
||||
IsNotificationAllowed: '' as Resource<
|
||||
(type: BaseNotificationType, providerId: Ref<NotificationProvider>) => boolean
|
||||
>
|
||||
IsNotificationAllowed: '' as Resource<(type: NotificationType, providerId: Ref<NotificationProvider>) => boolean>
|
||||
},
|
||||
resolver: {
|
||||
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { showPopup, resizeObserver, deviceOptionsStore as deviceInfo, PopupResult } from '@hcengineering/ui'
|
||||
import { Ref, Class, Doc } from '@hcengineering/core'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import MentionPopup from './MentionPopup.svelte'
|
||||
import DummyPopup from './DummyPopup.svelte'
|
||||
|
||||
export let docClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let query: string = ''
|
||||
export let multipleMentions: boolean = false
|
||||
export let clientRect: () => ClientRect
|
||||
export let command: (props: any) => void
|
||||
export let close: () => void
|
||||
@@ -115,7 +118,9 @@
|
||||
>
|
||||
<MentionPopup
|
||||
bind:this={searchPopup}
|
||||
{docClass}
|
||||
{query}
|
||||
{multipleMentions}
|
||||
on:close={(evt) => {
|
||||
dispatchItem(evt.detail)
|
||||
}}
|
||||
|
||||
@@ -14,23 +14,95 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { SearchResultDoc } from '@hcengineering/core'
|
||||
import presentation, { SearchResult, reduceCalls, searchFor, type SearchItem } from '@hcengineering/presentation'
|
||||
import core, { SearchResultDoc, Ref, Class, Doc } from '@hcengineering/core'
|
||||
import presentation, {
|
||||
SearchResult,
|
||||
reduceCalls,
|
||||
searchFor,
|
||||
type SearchItem,
|
||||
getClient
|
||||
} from '@hcengineering/presentation'
|
||||
import { Label, ListView, resizeObserver } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { getReferenceLabel, getReferenceObject } from './extension/reference'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
|
||||
export let query: string = ''
|
||||
export let multipleMentions: boolean = false
|
||||
export let docClass: Ref<Class<Doc>> | undefined = undefined
|
||||
|
||||
let items: SearchItem[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
let list: ListView
|
||||
let scrollContainer: HTMLElement
|
||||
let selection = 0
|
||||
|
||||
const employeeSearchCategory = client
|
||||
.getModel()
|
||||
.findAllSync(presentation.class.ObjectSearchCategory, { classToSearch: contact.mixin.Employee })[0]
|
||||
|
||||
async function getMultipleEmployeeSearchItems (localQuery: string, lastIndex: number): Promise<SearchItem[]> {
|
||||
if (!multipleMentions) return []
|
||||
const clazz =
|
||||
docClass && client.getHierarchy().hasClass(docClass) ? client.getHierarchy().getClass(docClass) : undefined
|
||||
const docTitle = await translate(clazz?.label ?? core.string.Object, {})
|
||||
|
||||
const everyoneDescription = await translate(contact.string.EveryoneDescription, {
|
||||
title: docTitle.toLowerCase()
|
||||
})
|
||||
const hereDescription = await translate(contact.string.HereDescription, {
|
||||
title: docTitle.toLowerCase()
|
||||
})
|
||||
const everyoneTitle = await translate(contact.string.Everyone, {})
|
||||
const hereTitle = await translate(contact.string.Here, {})
|
||||
return [
|
||||
{
|
||||
num: 0,
|
||||
category: employeeSearchCategory,
|
||||
item: {
|
||||
id: contact.mention.Everyone,
|
||||
title: everyoneTitle,
|
||||
description: everyoneDescription,
|
||||
emojiIcon: '📢',
|
||||
doc: {
|
||||
_id: contact.mention.Everyone,
|
||||
_class: contact.mixin.Employee
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
num: 0,
|
||||
category: employeeSearchCategory,
|
||||
item: {
|
||||
id: contact.mention.Here,
|
||||
title: hereTitle,
|
||||
description: hereDescription,
|
||||
emojiIcon: '📢',
|
||||
doc: {
|
||||
_id: contact.mention.Here,
|
||||
_class: contact.mixin.Employee
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
.filter((it) => it.item.title.toLowerCase().includes(localQuery.toLowerCase()))
|
||||
.map((it, idx) => ({ ...it, num: lastIndex + 1 + idx }))
|
||||
}
|
||||
|
||||
async function handleSelectItem (item: SearchResultDoc): Promise<void> {
|
||||
if ([contact.mention.Here, contact.mention.Everyone].includes(item.id as any)) {
|
||||
dispatch('close', {
|
||||
id: item.doc._id,
|
||||
label: item.title?.toLowerCase() ?? '',
|
||||
objectclass: item.doc._class
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const obj = (await getReferenceObject(item.doc._class, item.doc._id)) ?? item.doc
|
||||
const label = await getReferenceLabel(obj._class, obj._id)
|
||||
dispatch('close', {
|
||||
@@ -73,7 +145,13 @@
|
||||
const updateItems = reduceCalls(async function (localQuery: string): Promise<void> {
|
||||
const r = await searchFor('mention', localQuery)
|
||||
if (r.query === query) {
|
||||
items = r.items
|
||||
const latestIndex = r.items.findLastIndex((it) => it.category.classToSearch === contact.mixin.Employee)
|
||||
const multipleEmployeeSearchItems = await getMultipleEmployeeSearchItems(localQuery, latestIndex)
|
||||
|
||||
items =
|
||||
latestIndex === -1
|
||||
? [...multipleEmployeeSearchItems, ...r.items]
|
||||
: [...r.items.slice(0, latestIndex + 1), ...multipleEmployeeSearchItems, ...r.items.slice(latestIndex + 1)]
|
||||
}
|
||||
})
|
||||
$: void updateItems(query)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { Class, Doc, Markup, Ref } from '@hcengineering/core'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
|
||||
import textEditor, { RefAction, TextEditorHandler } from '@hcengineering/text-editor'
|
||||
@@ -61,6 +61,7 @@
|
||||
export let canEmbedImages = true
|
||||
export let onPaste: ((view: EditorView, event: ClipboardEvent) => boolean) | undefined = undefined
|
||||
export let onCancel: (() => void) | undefined = undefined
|
||||
export let docClass: Ref<Class<Doc>> | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const buttonSize = 'medium'
|
||||
@@ -142,6 +143,8 @@
|
||||
}
|
||||
const completionPlugin = ReferenceExtension.configure({
|
||||
...referenceConfig,
|
||||
docClass,
|
||||
multipleMentions: true,
|
||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||
dispatch('open-document', { event, _id, _class })
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ import workbench, { type Application } from '@hcengineering/workbench'
|
||||
|
||||
export interface ReferenceExtensionOptions extends ReferenceOptions {
|
||||
suggestion: Omit<SuggestionOptions, 'editor'>
|
||||
docClass?: Ref<Class<Doc>>
|
||||
multipleMentions?: boolean
|
||||
showDoc?: (event: MouseEvent, _id: string, _class: string) => void
|
||||
}
|
||||
|
||||
@@ -96,10 +98,12 @@ export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes
|
||||
)
|
||||
const withoutDoc = [contact.mention.Everyone, contact.mention.Here].includes(node.attrs.id)
|
||||
const id = node.attrs.id
|
||||
const objectclass: Ref<Class<Doc>> = node.attrs.objectclass
|
||||
|
||||
root.addEventListener('click', (event) => {
|
||||
if (withoutDoc) return
|
||||
if (event.button !== 0) return
|
||||
if (broken) {
|
||||
showPopup(MessageBox, {
|
||||
@@ -164,7 +168,7 @@ export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions
|
||||
const titleSpan = root.appendChild(document.createElement('span'))
|
||||
renderLabel({ id, objectclass, label: node.attrs.label })
|
||||
|
||||
if (id !== undefined && objectclass !== undefined) {
|
||||
if (id !== undefined && objectclass !== undefined && !withoutDoc) {
|
||||
query.query(objectclass, { _id: id }, async (result) => {
|
||||
const obj = result[0]
|
||||
broken = obj === undefined
|
||||
@@ -180,6 +184,8 @@ export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions
|
||||
renderLabel({ id, objectclass, label })
|
||||
}
|
||||
})
|
||||
} else if (withoutDoc) {
|
||||
query.unsubscribe()
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -228,6 +234,8 @@ export const ReferenceExtension = ReferenceNode.extend<ReferenceExtensionOptions
|
||||
return [
|
||||
Suggestion({
|
||||
editor: this.editor,
|
||||
docClass: this.options.docClass,
|
||||
multipleMentions: this.options.multipleMentions,
|
||||
...this.options.suggestion
|
||||
}),
|
||||
// ReferenceClickHandler(this.options),
|
||||
|
||||
@@ -2,8 +2,8 @@ import { type Editor, type Range, escapeForRegEx } from '@tiptap/core'
|
||||
import { type EditorState, Plugin, PluginKey, type Transaction } from '@tiptap/pm/state'
|
||||
import { ReplaceStep } from '@tiptap/pm/transform'
|
||||
import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
|
||||
|
||||
import { type ResolvedPos } from '@tiptap/pm/model'
|
||||
import { type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
|
||||
export interface Trigger {
|
||||
char: string
|
||||
@@ -129,6 +129,8 @@ export interface SuggestionOptions<I = any> {
|
||||
startOfLine?: boolean
|
||||
decorationTag?: string
|
||||
decorationClass?: string
|
||||
multipleMentions?: boolean
|
||||
docClass?: Ref<Class<Doc>>
|
||||
command?: (props: { editor: Editor, range: Range, props: I }) => void
|
||||
items?: (props: { query: string, editor: Editor }) => I[] | Promise<I[]>
|
||||
render?: () => {
|
||||
@@ -147,6 +149,8 @@ export interface SuggestionProps<I = any> {
|
||||
range: Range
|
||||
query: string
|
||||
text: string
|
||||
multipleMentions: boolean
|
||||
docClass?: Ref<Class<Doc>>
|
||||
items: I[]
|
||||
command: (props: I) => void
|
||||
decorationNode: Element | null
|
||||
@@ -172,6 +176,8 @@ export default function Suggestion<I = any> ({
|
||||
startOfLine = false,
|
||||
decorationTag = 'span',
|
||||
decorationClass = 'suggestion',
|
||||
multipleMentions = false,
|
||||
docClass,
|
||||
command = () => null,
|
||||
items = () => [],
|
||||
render = () => ({}),
|
||||
@@ -226,7 +232,9 @@ export default function Suggestion<I = any> ({
|
||||
range: state.range,
|
||||
query: state.query,
|
||||
text: state.text,
|
||||
docClass,
|
||||
items: [],
|
||||
multipleMentions,
|
||||
command: (commandProps) => {
|
||||
command({
|
||||
editor,
|
||||
|
||||
@@ -37,19 +37,18 @@ import core, {
|
||||
type MeasureContext,
|
||||
AccountUuid
|
||||
} from '@hcengineering/core'
|
||||
import notification, { MentionInboxNotification } from '@hcengineering/notification'
|
||||
import notification, { MentionInboxNotification, NotificationType } from '@hcengineering/notification'
|
||||
import { getPerson } from '@hcengineering/server-contact'
|
||||
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
|
||||
import {
|
||||
getCommonNotificationTxes,
|
||||
getNotificationProviderControl,
|
||||
getPushCollaboratorTx,
|
||||
NotifyResult,
|
||||
shouldNotifyCommon,
|
||||
type NotificationProviderControl,
|
||||
isShouldNotifyTx
|
||||
getAllowedProviders,
|
||||
type NotificationProviderControl
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
import { areEqualJson, extractReferences, jsonToMarkup, markupToJSON } from '@hcengineering/text-core'
|
||||
import { getReceiversInfo } from '@hcengineering/server-notification-resources'
|
||||
|
||||
export function isDocMentioned (doc: Ref<Doc>, content: string): boolean {
|
||||
const references = []
|
||||
@@ -75,51 +74,9 @@ export async function getPersonNotificationTxes (
|
||||
originTx: TxCUD<Doc>,
|
||||
notificationControl: NotificationProviderControl
|
||||
): Promise<Tx[]> {
|
||||
const receiverPersonRef = reference.attachedTo as Ref<Person>
|
||||
const receiverSocialIdentity = await control.findAll(ctx, contact.class.SocialIdentity, {
|
||||
attachedTo: receiverPersonRef
|
||||
})
|
||||
const receiverSocialIds = receiverSocialIdentity.map((si) => si._id) as PersonId[]
|
||||
|
||||
if (receiverSocialIds.includes(senderId)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const receiverEmployee = (
|
||||
await control.findAll(
|
||||
ctx,
|
||||
contact.mixin.Employee,
|
||||
{ _id: receiverPersonRef as Ref<Employee>, active: true },
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
const receiverAccount = receiverEmployee?.personUuid
|
||||
if (receiverAccount == null) {
|
||||
return []
|
||||
}
|
||||
|
||||
const { hierarchy } = control
|
||||
const res: Tx[] = []
|
||||
const isAvailable = await checkSpace(receiverAccount, space, control, res)
|
||||
|
||||
if (!isAvailable) {
|
||||
return []
|
||||
}
|
||||
|
||||
const doc = (await control.findAll(ctx, reference.srcDocClass, { _id: reference.srcDocId }))[0]
|
||||
|
||||
const receiverSpace = (
|
||||
await control.findAll(ctx, contact.class.PersonSpace, { person: receiverPersonRef }, { limit: 1 })
|
||||
)[0]
|
||||
if (receiverSpace === undefined) return res
|
||||
|
||||
const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiverAccount, doc)
|
||||
|
||||
res.push(...collaboratorsTx)
|
||||
|
||||
if (doc === undefined) {
|
||||
return res
|
||||
}
|
||||
|
||||
const receiverPersonRef = reference.attachedTo as Ref<Person>
|
||||
const info = (
|
||||
await control.findAll<UserMentionInfo>(ctx, activity.class.UserMentionInfo, {
|
||||
user: receiverPersonRef,
|
||||
@@ -127,101 +84,138 @@ export async function getPersonNotificationTxes (
|
||||
})
|
||||
)[0]
|
||||
|
||||
if (info === undefined) {
|
||||
res.push(
|
||||
control.txFactory.createTxCreateDoc(activity.class.UserMentionInfo, space, {
|
||||
attachedTo: reference.attachedDocId ?? reference.srcDocId,
|
||||
attachedToClass: reference.attachedDocClass ?? reference.srcDocClass,
|
||||
user: receiverPersonRef,
|
||||
content: reference.message,
|
||||
collection: 'mentions'
|
||||
})
|
||||
)
|
||||
res.push(getUpdateMentionInfoTx(control, reference, space, info))
|
||||
|
||||
if (
|
||||
originTx._class === core.class.TxCreateDoc &&
|
||||
hierarchy.isDerived(originTx.objectClass, activity.class.ActivityMessage)
|
||||
) {
|
||||
return res
|
||||
}
|
||||
|
||||
if (info !== undefined && hierarchy.isDerived(originTx.objectClass, activity.class.ActivityMessage)) {
|
||||
return res
|
||||
}
|
||||
|
||||
const doc = (await control.findAll(ctx, reference.srcDocClass, { _id: reference.srcDocId }))[0]
|
||||
if (doc === undefined) return res
|
||||
const docSpace = (await control.findAll<Space>(control.ctx, core.class.Space, { _id: space }, { limit: 1 }))[0]
|
||||
if (docSpace === undefined) return res
|
||||
|
||||
const senderAccount = control.ctx.contextData.socialStringsToUsers.get(senderId)
|
||||
|
||||
let collaborators: AccountUuid[] = []
|
||||
|
||||
if ([contact.mention.Everyone, contact.mention.Here].includes(reference.attachedTo as Ref<Employee>)) {
|
||||
collaborators = await getMultipleMentionCollaborators(reference, control, doc)
|
||||
} else {
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(info._class, info.space, info._id, {
|
||||
content: reference.message
|
||||
})
|
||||
)
|
||||
const employee = (
|
||||
await control.findAll(ctx, contact.mixin.Employee, { _id: reference.attachedTo as Ref<Employee> })
|
||||
)[0]
|
||||
if (employee?.personUuid != null && employee.personUuid !== senderAccount) {
|
||||
collaborators = [employee.personUuid]
|
||||
|
||||
const collaboratorsTx = getCollaboratorsTxes(control, employee.personUuid, doc)
|
||||
|
||||
res.push(...collaboratorsTx)
|
||||
}
|
||||
}
|
||||
|
||||
const data: Omit<Data<MentionInboxNotification>, 'docNotifyContext'> = {
|
||||
header: activity.string.MentionedYouIn,
|
||||
messageHtml: reference.message,
|
||||
mentionedIn: reference.attachedDocId ?? reference.srcDocId,
|
||||
mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass,
|
||||
objectId: reference.srcDocId,
|
||||
objectClass: reference.srcDocClass,
|
||||
user: receiverAccount,
|
||||
isViewed: false,
|
||||
archived: false
|
||||
}
|
||||
const filteredCollaborators = collaborators.filter(
|
||||
(c) => c !== senderAccount && checkSpace(c, docSpace, control, res)
|
||||
)
|
||||
|
||||
if (filteredCollaborators.length === 0) return res
|
||||
const receivers = await getReceiversInfo(ctx, filteredCollaborators, control)
|
||||
if (receivers.length === 0) return []
|
||||
const senderPerson = await getPerson(control, senderId)
|
||||
|
||||
const receiver = {
|
||||
account: receiverAccount,
|
||||
socialIds: receiverSocialIds,
|
||||
space: receiverSpace._id,
|
||||
employee: receiverEmployee._id
|
||||
}
|
||||
const sender = {
|
||||
socialId: senderId,
|
||||
person: senderPerson
|
||||
}
|
||||
const type: NotificationType = control.modelDb.findAllSync(notification.class.NotificationType, {
|
||||
_id: notification.ids.MentionNotificationType
|
||||
})[0]
|
||||
for (const receiver of receivers) {
|
||||
const data: Omit<Data<MentionInboxNotification>, 'docNotifyContext'> = {
|
||||
header: activity.string.MentionedYouIn,
|
||||
messageHtml: reference.message,
|
||||
mentionedIn: reference.attachedDocId ?? reference.srcDocId,
|
||||
mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass,
|
||||
objectId: reference.srcDocId,
|
||||
objectClass: reference.srcDocClass,
|
||||
user: receiver.account,
|
||||
isViewed: false,
|
||||
archived: false
|
||||
}
|
||||
|
||||
const notifyResult = await shouldNotifyCommon(
|
||||
control,
|
||||
receiverSocialIds,
|
||||
notification.ids.MentionCommonNotificationType,
|
||||
notificationControl
|
||||
)
|
||||
const messageNotifyResult = await getMessageNotifyResult(
|
||||
reference,
|
||||
receiverAccount,
|
||||
receiverEmployee,
|
||||
receiverSocialIds,
|
||||
control,
|
||||
originTx,
|
||||
doc,
|
||||
notificationControl
|
||||
)
|
||||
const allowedProviders = getAllowedProviders(control, receiver.socialIds, type, notificationControl)
|
||||
const notifyResult = new Map(allowedProviders.map((it) => [it, [type]]))
|
||||
|
||||
for (const [provider] of messageNotifyResult.entries()) {
|
||||
if (notifyResult.has(provider)) {
|
||||
notifyResult.delete(provider)
|
||||
if (notifyResult.has(notification.providers.InboxNotificationProvider)) {
|
||||
const txes = await getCommonNotificationTxes(
|
||||
control.ctx,
|
||||
control,
|
||||
doc,
|
||||
data,
|
||||
receiver,
|
||||
sender,
|
||||
reference.srcDocId,
|
||||
reference.srcDocClass,
|
||||
doc.space,
|
||||
originTx.modifiedOn,
|
||||
notifyResult,
|
||||
notification.class.MentionInboxNotification,
|
||||
originTx
|
||||
)
|
||||
res.push(...txes)
|
||||
}
|
||||
}
|
||||
|
||||
if (notifyResult.has(notification.providers.InboxNotificationProvider)) {
|
||||
const txes = await getCommonNotificationTxes(
|
||||
ctx,
|
||||
control,
|
||||
doc,
|
||||
data,
|
||||
receiver,
|
||||
sender,
|
||||
reference.srcDocId,
|
||||
reference.srcDocClass,
|
||||
doc.space,
|
||||
originTx.modifiedOn,
|
||||
notifyResult,
|
||||
notification.class.MentionInboxNotification,
|
||||
originTx
|
||||
)
|
||||
res.push(...txes)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function checkSpace (
|
||||
account: AccountUuid,
|
||||
spaceId: Ref<Space>,
|
||||
function getUpdateMentionInfoTx (
|
||||
control: TriggerControl,
|
||||
res: Tx[]
|
||||
): Promise<boolean> {
|
||||
const space = (await control.findAll<Space>(control.ctx, core.class.Space, { _id: spaceId }, { limit: 1 }))[0]
|
||||
reference: Data<ActivityReference>,
|
||||
space: Ref<Space>,
|
||||
info?: UserMentionInfo
|
||||
): Tx {
|
||||
if (info === undefined) {
|
||||
return control.txFactory.createTxCreateDoc(activity.class.UserMentionInfo, space, {
|
||||
attachedTo: reference.attachedDocId ?? reference.srcDocId,
|
||||
attachedToClass: reference.attachedDocClass ?? reference.srcDocClass,
|
||||
user: reference.attachedTo as Ref<Person>,
|
||||
content: reference.message,
|
||||
collection: 'mentions'
|
||||
})
|
||||
}
|
||||
|
||||
return control.txFactory.createTxUpdateDoc(info._class, info.space, info._id, {
|
||||
content: reference.message
|
||||
})
|
||||
}
|
||||
|
||||
async function getMultipleMentionCollaborators (
|
||||
reference: Data<ActivityReference>,
|
||||
control: TriggerControl,
|
||||
doc: Doc
|
||||
): Promise<AccountUuid[]> {
|
||||
const { hierarchy } = control
|
||||
const personRef = reference.attachedTo as Ref<Person>
|
||||
|
||||
const mixin = hierarchy.classHierarchyMixin(doc._class, notification.mixin.ClassCollaborators)
|
||||
if (mixin === undefined) return []
|
||||
const collaborators = hierarchy.as(doc, notification.mixin.Collaborators).collaborators
|
||||
|
||||
if (collaborators.length === 0) return []
|
||||
const statuses = Array.from(control.userStatusMap.values())
|
||||
|
||||
return personRef === contact.mention.Here
|
||||
? collaborators.filter((it) => statuses.some((s) => s.online && s.user === it))
|
||||
: collaborators
|
||||
}
|
||||
|
||||
function checkSpace (account: AccountUuid, space: Space, control: TriggerControl, res: Tx[]): boolean {
|
||||
const isMember = space.members.includes(account)
|
||||
|
||||
if (space.private) {
|
||||
@@ -235,13 +229,7 @@ async function checkSpace (
|
||||
return true
|
||||
}
|
||||
|
||||
async function getCollaboratorsTxes (
|
||||
reference: Data<ActivityReference>,
|
||||
control: TriggerControl,
|
||||
receiver: AccountUuid,
|
||||
object?: Doc
|
||||
): Promise<TxMixin<Doc, Doc>[]> {
|
||||
const { hierarchy } = control
|
||||
function getCollaboratorsTxes (control: TriggerControl, receiver: AccountUuid, object?: Doc): TxMixin<Doc, Doc>[] {
|
||||
const res: TxMixin<Doc, Doc>[] = []
|
||||
|
||||
if (object !== undefined) {
|
||||
@@ -253,72 +241,9 @@ async function getCollaboratorsTxes (
|
||||
}
|
||||
}
|
||||
|
||||
if (reference.attachedDocClass === undefined || reference.attachedDocId === undefined) {
|
||||
return res
|
||||
}
|
||||
|
||||
if (!hierarchy.isDerived(reference.attachedDocClass, activity.class.ActivityMessage)) {
|
||||
return res
|
||||
}
|
||||
|
||||
const message = (
|
||||
await control.findAll<ActivityMessage>(
|
||||
control.ctx,
|
||||
reference.attachedDocClass,
|
||||
{
|
||||
_id: reference.attachedDocId as Ref<ActivityMessage>
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
|
||||
if (message === undefined) {
|
||||
return res
|
||||
}
|
||||
|
||||
// Add user to collaborators of message where user is mentioned
|
||||
const messageTx = getPushCollaboratorTx(control, receiver, message)
|
||||
|
||||
if (messageTx !== undefined) {
|
||||
res.push(messageTx)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function getMessageNotifyResult (
|
||||
reference: Data<ActivityReference>,
|
||||
account: AccountUuid,
|
||||
person: Person,
|
||||
personIds: PersonId[],
|
||||
control: TriggerControl,
|
||||
tx: TxCUD<Doc>,
|
||||
doc: Doc,
|
||||
notificationControl: NotificationProviderControl
|
||||
): Promise<NotifyResult> {
|
||||
const { hierarchy } = control
|
||||
|
||||
if (
|
||||
reference.attachedDocClass === undefined ||
|
||||
reference.attachedDocId === undefined ||
|
||||
tx._class !== core.class.TxCreateDoc
|
||||
) {
|
||||
return new Map()
|
||||
}
|
||||
|
||||
const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators)
|
||||
|
||||
if (mixin === undefined || !mixin.collaborators.includes(account)) {
|
||||
return new Map()
|
||||
}
|
||||
|
||||
if (!hierarchy.isDerived(reference.attachedDocClass, activity.class.ActivityMessage)) {
|
||||
return new Map()
|
||||
}
|
||||
|
||||
return await isShouldNotifyTx(control, tx, doc, person._id, personIds, false, false, notificationControl, undefined)
|
||||
}
|
||||
|
||||
function isMarkupType (type: Ref<Class<Type<any>>>): boolean {
|
||||
return type === core.class.TypeMarkup
|
||||
}
|
||||
@@ -485,6 +410,41 @@ async function createReferenceTxes (
|
||||
return [tx]
|
||||
}
|
||||
|
||||
async function getRemoveMentionTxes (
|
||||
control: TriggerControl,
|
||||
mention: UserMentionInfo,
|
||||
originTx: TxCUD<Doc>
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
res.push(control.txFactory.createTxRemoveDoc(mention._class, mention.space, mention._id))
|
||||
|
||||
if (control.hierarchy.isDerived(originTx.objectClass, activity.class.ActivityMessage)) {
|
||||
const _id = originTx.objectId as Ref<ActivityMessage>
|
||||
const person = (
|
||||
await control.findAll(control.ctx, contact.mixin.Employee, { _id: mention.user as Ref<Employee> })
|
||||
)[0]
|
||||
if (person?.personUuid !== undefined) {
|
||||
const activityNotification = await control.findAll(control.ctx, notification.class.ActivityInboxNotification, {
|
||||
attachedTo: _id,
|
||||
user: person.personUuid
|
||||
})
|
||||
const mentionNotifications = await control.findAll(control.ctx, notification.class.MentionInboxNotification, {
|
||||
mentionedIn: _id,
|
||||
user: person.personUuid
|
||||
})
|
||||
res.push(
|
||||
...activityNotification
|
||||
.filter((it) => it.types?.length === 1 && it.types[0] === notification.ids.MentionNotificationType)
|
||||
.map((it) => control.txFactory.createTxRemoveDoc(it._class, it.space, it._id))
|
||||
)
|
||||
|
||||
res.push(...mentionNotifications.map((it) => control.txFactory.createTxRemoveDoc(it._class, it.space, it._id)))
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function getReferencesTxes (
|
||||
ctx: MeasureContext,
|
||||
control: TriggerControl,
|
||||
@@ -535,7 +495,8 @@ async function getReferencesTxes (
|
||||
references.splice(refIndex, 1)
|
||||
}
|
||||
} else {
|
||||
txes.push(txFactory.createTxRemoveDoc(mention._class, mention.space, mention._id))
|
||||
const removeTxes = await getRemoveMentionTxes(control, mention, originTx)
|
||||
txes.push(...removeTxes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,6 +523,14 @@ async function getRemoveActivityReferenceTxes (
|
||||
attachedTo: removedDocId
|
||||
})
|
||||
|
||||
const notifications = await control.findAll(control.ctx, notification.class.MentionInboxNotification, {
|
||||
mentionedIn: removedDocId
|
||||
})
|
||||
|
||||
for (const notification of notifications) {
|
||||
const removeTx = txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id)
|
||||
txes.push(removeTx)
|
||||
}
|
||||
for (const ref of refs) {
|
||||
const removeTx = txFactory.createTxRemoveDoc(ref._class, ref.space, ref._id)
|
||||
txes.push(txFactory.createTxCollectionCUD(ref.attachedToClass, ref.attachedTo, ref.space, ref.collection, removeTx))
|
||||
@@ -727,12 +696,9 @@ export async function ReferenceTrigger (txes: TxCUD<Doc>[], control: TriggerCont
|
||||
const result: Tx[] = []
|
||||
|
||||
for (const tx of txes) {
|
||||
if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityReference)) {
|
||||
continue
|
||||
}
|
||||
if (control.hierarchy.isDerived(tx.objectClass, notification.class.InboxNotification)) {
|
||||
continue
|
||||
}
|
||||
if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityReference)) continue
|
||||
if (control.hierarchy.isDerived(tx.objectClass, notification.class.InboxNotification)) continue
|
||||
if (control.hierarchy.isDerived(tx.objectClass, activity.class.UserMentionInfo)) continue
|
||||
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
result.push(...(await ActivityReferenceCreate(tx, control)))
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
|
||||
import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter'
|
||||
import contact, { Person } from '@hcengineering/contact'
|
||||
import contact, { Employee, Person } from '@hcengineering/contact'
|
||||
import { getAccountBySocialId, getPerson } from '@hcengineering/server-contact'
|
||||
import core, {
|
||||
PersonId,
|
||||
@@ -36,7 +36,8 @@ import core, {
|
||||
UserStatus,
|
||||
type MeasureContext,
|
||||
combineAttributes,
|
||||
AccountUuid
|
||||
AccountUuid,
|
||||
notEmpty
|
||||
} from '@hcengineering/core'
|
||||
import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification'
|
||||
import { getMetadata, IntlString, translate } from '@hcengineering/platform'
|
||||
@@ -46,7 +47,7 @@ import {
|
||||
getDocCollaborators,
|
||||
getMixinTx
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
import { markupToText, stripTags } from '@hcengineering/text-core'
|
||||
import { extractReferences, markupToText, stripTags } from '@hcengineering/text-core'
|
||||
import { jsonToHTML, markupToJSON } from '@hcengineering/text'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
|
||||
@@ -175,14 +176,21 @@ async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD<Doc>, contro
|
||||
const isChannel = hierarchy.isDerived(targetDoc._class, chunter.class.Channel)
|
||||
const res: Tx[] = []
|
||||
const account = await getAccountBySocialId(control, message.modifiedBy)
|
||||
|
||||
if (account == null) {
|
||||
return []
|
||||
}
|
||||
const node = markupToJSON(message.message)
|
||||
const references = extractReferences(node)
|
||||
const mentionedPersons = references
|
||||
.filter(({ objectClass }) => control.hierarchy.isDerived(objectClass, contact.class.Person))
|
||||
.map(({ objectId }) => objectId as Ref<Person>)
|
||||
const employees =
|
||||
mentionedPersons.length > 0
|
||||
? await control.findAll(ctx, contact.mixin.Employee, { _id: { $in: mentionedPersons as Ref<Employee>[] } })
|
||||
: []
|
||||
const collaboratorsFromMessage = [...employees.map((it) => it.personUuid), account].filter(notEmpty)
|
||||
|
||||
if (hierarchy.hasMixin(targetDoc, notification.mixin.Collaborators)) {
|
||||
const collaboratorsMixin = hierarchy.as(targetDoc, notification.mixin.Collaborators)
|
||||
if (!collaboratorsMixin.collaborators.includes(account)) {
|
||||
const newCollabs = collaboratorsFromMessage.filter((it) => !collaboratorsMixin.collaborators.includes(it))
|
||||
if (newCollabs.length > 0) {
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(
|
||||
targetDoc._id,
|
||||
@@ -190,22 +198,25 @@ async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD<Doc>, contro
|
||||
targetDoc.space,
|
||||
notification.mixin.Collaborators,
|
||||
{
|
||||
$push: {
|
||||
collaborators: account
|
||||
}
|
||||
$push: { collaborators: { $each: newCollabs, $position: 0 } }
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const collaborators = await getDocCollaborators(ctx, targetDoc, mixin, control)
|
||||
if (!collaborators.includes(account)) {
|
||||
collaborators.push(account)
|
||||
}
|
||||
res.push(getMixinTx(tx, control, collaborators))
|
||||
res.push(getMixinTx(tx, control, Array.from(new Set(collaborators.concat(collaboratorsFromMessage)))))
|
||||
}
|
||||
|
||||
if (isChannel && !(targetDoc as Channel).members.includes(account)) {
|
||||
if (collaboratorsFromMessage.length > 0) {
|
||||
control.txFactory.createTxMixin(message._id, message._class, message.space, notification.mixin.Collaborators, {
|
||||
$push: {
|
||||
$push: { collaborators: { $each: collaboratorsFromMessage, $position: 0 } }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (account != null && isChannel && !(targetDoc as Channel).members.includes(account)) {
|
||||
res.push(...joinChannel(control, targetDoc as Channel, account))
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
} from '@hcengineering/core'
|
||||
import gmail, { Message } from '@hcengineering/gmail'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
import { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification'
|
||||
import { NotificationType, InboxNotification } from '@hcengineering/notification'
|
||||
import serverNotification, { ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
|
||||
import { getContentByTemplate } from '@hcengineering/server-notification-resources'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
@@ -138,7 +138,7 @@ export async function sendEmailNotification (
|
||||
|
||||
async function notifyByEmail (
|
||||
control: TriggerControl,
|
||||
type: Ref<BaseNotificationType>,
|
||||
type: Ref<NotificationType>,
|
||||
doc: Doc | undefined,
|
||||
sender: SenderInfo,
|
||||
receiver: ReceiverInfo,
|
||||
@@ -160,7 +160,7 @@ async function notifyByEmail (
|
||||
|
||||
const SendEmailNotifications = async (
|
||||
control: TriggerControl,
|
||||
types: BaseNotificationType[],
|
||||
types: NotificationType[],
|
||||
object: Doc,
|
||||
data: InboxNotification,
|
||||
receiver: ReceiverInfo,
|
||||
|
||||
@@ -47,7 +47,6 @@ import core, {
|
||||
} from '@hcengineering/core'
|
||||
import notification, {
|
||||
ActivityInboxNotification,
|
||||
BaseNotificationType,
|
||||
ClassCollaborators,
|
||||
Collaborators,
|
||||
CommonInboxNotification,
|
||||
@@ -87,6 +86,7 @@ import {
|
||||
isMixinTx,
|
||||
isShouldNotifyTx,
|
||||
isUserEmployeeInFieldValueTypeMatch,
|
||||
mentionTypeMatch,
|
||||
messageToMarkup,
|
||||
type NotificationProviderControl,
|
||||
replaceAll,
|
||||
@@ -147,6 +147,7 @@ export async function getCommonNotificationTxes (
|
||||
data,
|
||||
_class,
|
||||
modifiedOn,
|
||||
[],
|
||||
true,
|
||||
tx
|
||||
)
|
||||
@@ -191,7 +192,7 @@ function fillTemplate (
|
||||
export async function getContentByTemplate (
|
||||
doc: Doc | undefined,
|
||||
sender: string,
|
||||
type: Ref<BaseNotificationType>,
|
||||
type: Ref<NotificationType>,
|
||||
control: TriggerControl,
|
||||
data: string,
|
||||
notificationData?: InboxNotification,
|
||||
@@ -352,6 +353,7 @@ export async function pushInboxNotifications (
|
||||
data: Partial<Data<InboxNotification>>,
|
||||
_class: Ref<Class<InboxNotification>>,
|
||||
modifiedOn: Timestamp,
|
||||
types: Ref<NotificationType>[],
|
||||
shouldUpdateTimestamp = true,
|
||||
tx?: TxCUD<Doc>
|
||||
): Promise<TxCreateDoc<InboxNotification> | undefined> {
|
||||
@@ -381,6 +383,7 @@ export async function pushInboxNotifications (
|
||||
archived: false,
|
||||
objectId,
|
||||
objectClass,
|
||||
types,
|
||||
...data
|
||||
}
|
||||
const notificationTx = control.txFactory.createTxCreateDoc(_class, receiver.space, notificationData)
|
||||
@@ -500,6 +503,7 @@ export async function pushActivityInboxNotifications (
|
||||
object: Doc,
|
||||
docNotifyContexts: DocNotifyContext[],
|
||||
activityMessage: ActivityMessage,
|
||||
types: Ref<NotificationType>[],
|
||||
shouldUpdateTimestamp: boolean
|
||||
): Promise<TxCreateDoc<InboxNotification> | undefined> {
|
||||
const content = await getNotificationContent(originTx, receiver.employee, sender, object, control)
|
||||
@@ -522,6 +526,7 @@ export async function pushActivityInboxNotifications (
|
||||
data,
|
||||
notification.class.ActivityInboxNotification,
|
||||
activityMessage.modifiedOn,
|
||||
types,
|
||||
shouldUpdateTimestamp,
|
||||
originTx
|
||||
)
|
||||
@@ -590,8 +595,7 @@ export async function getNotificationTxes (
|
||||
control,
|
||||
tx,
|
||||
object,
|
||||
receiver.employee,
|
||||
receiver.socialIds,
|
||||
receiver,
|
||||
params.isOwn,
|
||||
params.isSpace,
|
||||
settings,
|
||||
@@ -599,6 +603,7 @@ export async function getNotificationTxes (
|
||||
)
|
||||
|
||||
if (notifyResult.has(notification.providers.InboxNotificationProvider)) {
|
||||
const types = (notifyResult.get(notification.providers.InboxNotificationProvider) ?? []).map((it) => it._id)
|
||||
const notificationTx = await pushActivityInboxNotifications(
|
||||
ctx,
|
||||
tx,
|
||||
@@ -609,6 +614,7 @@ export async function getNotificationTxes (
|
||||
object,
|
||||
docNotifyContexts,
|
||||
message,
|
||||
types,
|
||||
params.shouldUpdateTimestamp
|
||||
)
|
||||
|
||||
@@ -753,7 +759,7 @@ export async function createCollabDocInfo (
|
||||
|
||||
cache.set(space._id, space)
|
||||
|
||||
const filteredCollaborators = control.hierarchy.isDerived(object._class, core.class.SystemSpace)
|
||||
const filteredCollaborators = !space.private
|
||||
? collaborators
|
||||
: collaborators.filter(
|
||||
(it) =>
|
||||
@@ -1028,7 +1034,7 @@ async function updateCollaboratorsMixin (
|
||||
prevCollabs = mixin !== undefined ? new Set(await getDocCollaborators(ctx, prevDoc, mixin, control)) : new Set()
|
||||
}
|
||||
|
||||
const type = await control.modelDb.findOne(notification.class.BaseNotificationType, {
|
||||
const type = await control.modelDb.findOne(notification.class.NotificationType, {
|
||||
_id: notification.ids.CollaboratoAddNotification
|
||||
})
|
||||
|
||||
@@ -1091,6 +1097,7 @@ async function updateCollaboratorsMixin (
|
||||
prevDoc,
|
||||
docNotifyContexts,
|
||||
message,
|
||||
[],
|
||||
true
|
||||
)
|
||||
}
|
||||
@@ -1712,6 +1719,7 @@ export default async () => ({
|
||||
PushNotificationsHandler
|
||||
},
|
||||
function: {
|
||||
IsUserEmployeeInFieldValueTypeMatch: isUserEmployeeInFieldValueTypeMatch
|
||||
IsUserEmployeeInFieldValueTypeMatch: isUserEmployeeInFieldValueTypeMatch,
|
||||
MentionTypeMatch: mentionTypeMatch
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
import {
|
||||
BaseNotificationType,
|
||||
NotificationType,
|
||||
DocNotifyContext,
|
||||
InboxNotification,
|
||||
NotificationProvider
|
||||
@@ -32,7 +32,7 @@ export interface Content {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type NotifyResult = Map<Ref<NotificationProvider>, BaseNotificationType[]>
|
||||
export type NotifyResult = Map<Ref<NotificationProvider>, NotificationType[]>
|
||||
|
||||
export interface NotifyParams {
|
||||
isOwn: boolean
|
||||
|
||||
@@ -40,14 +40,14 @@ import core, {
|
||||
Ref,
|
||||
Space,
|
||||
Tx,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxUpdateDoc
|
||||
} from '@hcengineering/core'
|
||||
import notification, {
|
||||
BaseNotificationType,
|
||||
Collaborators,
|
||||
CommonNotificationType,
|
||||
NotificationContent,
|
||||
notificationId,
|
||||
NotificationProvider,
|
||||
@@ -67,9 +67,10 @@ import serverNotification, {
|
||||
import serverView from '@hcengineering/server-view'
|
||||
import { encodeObjectURI } from '@hcengineering/view'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import { extractReferences, markupToJSON, Reference } from '@hcengineering/text-core'
|
||||
import { getPersonSpaces } from '@hcengineering/server-contact'
|
||||
|
||||
import { NotifyResult } from './types'
|
||||
import { getPersonSpaces } from '@hcengineering/server-contact'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -94,6 +95,36 @@ export function isUserEmployeeInFieldValueTypeMatch (
|
||||
}
|
||||
}
|
||||
|
||||
export const mentionTypeMatch = (
|
||||
tx: TxCreateDoc<ChatMessage>,
|
||||
doc: Doc,
|
||||
person: Ref<Person>,
|
||||
socialIds: PersonId[],
|
||||
type: NotificationType,
|
||||
control: TriggerControl,
|
||||
account: AccountUuid
|
||||
): boolean => {
|
||||
const hierarchy = control.hierarchy
|
||||
if (tx._class !== core.class.TxCreateDoc) return false
|
||||
if (!hierarchy.isDerived(tx.objectClass, chunter.class.ChatMessage)) return false
|
||||
const message = TxProcessor.createDoc2Doc(tx)
|
||||
const content: string = message.message
|
||||
|
||||
const references: Reference[] =
|
||||
control.contextCache.get(`${message._id}_references`) ?? extractReferences(markupToJSON(content))
|
||||
control.contextCache.set(`${message._id}_references`, references)
|
||||
|
||||
if (references.length === 0) return false
|
||||
|
||||
if (references.some(({ objectId }) => objectId === contact.mention.Everyone)) return true
|
||||
if (references.some(({ objectId }) => objectId === contact.mention.Here)) {
|
||||
const isOnline = Array.from(control.userStatusMap.values()).some(({ user, online }) => user === account && online)
|
||||
if (isOnline) return true
|
||||
}
|
||||
|
||||
return references.some(({ objectId }) => objectId === person)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -119,27 +150,20 @@ function escapeRegExp (str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
export async function shouldNotifyCommon (
|
||||
export function getAllowedProviders (
|
||||
control: TriggerControl,
|
||||
socialIds: PersonId[],
|
||||
typeId: Ref<CommonNotificationType>,
|
||||
type: NotificationType,
|
||||
notificationControl: NotificationProviderControl
|
||||
): Promise<NotifyResult> {
|
||||
const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0]
|
||||
|
||||
if (type === undefined) {
|
||||
return new Map()
|
||||
}
|
||||
|
||||
const result = new Map<Ref<NotificationProvider>, BaseNotificationType[]>()
|
||||
const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {})
|
||||
): Ref<NotificationProvider>[] {
|
||||
const result: Ref<NotificationProvider>[] = []
|
||||
const providers = control.modelDb.findAllSync(notification.class.NotificationProvider, {})
|
||||
|
||||
for (const provider of providers) {
|
||||
const allowed = isAllowed(control, socialIds, type, provider, notificationControl)
|
||||
|
||||
if (allowed) {
|
||||
const cur = result.get(provider._id) ?? []
|
||||
result.set(provider._id, [...cur, type])
|
||||
result.push(provider._id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +173,7 @@ export async function shouldNotifyCommon (
|
||||
export function isAllowed (
|
||||
control: TriggerControl,
|
||||
receiverIds: PersonId[],
|
||||
type: BaseNotificationType,
|
||||
type: NotificationType,
|
||||
provider: NotificationProvider,
|
||||
notificationControl: NotificationProviderControl
|
||||
): boolean {
|
||||
@@ -192,8 +216,7 @@ export async function isShouldNotifyTx (
|
||||
control: TriggerControl,
|
||||
tx: TxCUD<Doc>,
|
||||
object: Doc,
|
||||
person: Ref<Person>,
|
||||
personIds: PersonId[],
|
||||
receiver: ReceiverInfo,
|
||||
isOwn: boolean,
|
||||
isSpace: boolean,
|
||||
notificationControl: NotificationProviderControl,
|
||||
@@ -201,7 +224,7 @@ export async function isShouldNotifyTx (
|
||||
): Promise<NotifyResult> {
|
||||
const types = getMatchedTypes(control, tx, isOwn, isSpace, docUpdateMessage?.attributeUpdates?.attrKey)
|
||||
const modifiedByPersonId = tx.modifiedBy
|
||||
const result = new Map<Ref<NotificationProvider>, BaseNotificationType[]>()
|
||||
const result = new Map<Ref<NotificationProvider>, NotificationType[]>()
|
||||
let providers: NotificationProvider[] = control.modelDb.findAllSync(notification.class.NotificationProvider, {})
|
||||
|
||||
if (getMetadata(serverNotification.metadata.InboxOnlyNotifications) === true) {
|
||||
@@ -209,7 +232,7 @@ export async function isShouldNotifyTx (
|
||||
}
|
||||
|
||||
for (const type of types) {
|
||||
if (type.allowedForAuthor !== true && personIds.includes(modifiedByPersonId)) {
|
||||
if (type.allowedForAuthor !== true && receiver.socialIds.includes(modifiedByPersonId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -217,7 +240,7 @@ export async function isShouldNotifyTx (
|
||||
const mixin = control.hierarchy.as(type, serverNotification.mixin.TypeMatch)
|
||||
if (mixin.func !== undefined) {
|
||||
const f = await getResource(mixin.func)
|
||||
let res = f(tx, object, person, personIds, type, control)
|
||||
let res = f(tx, object, receiver.employee, receiver.socialIds, type, control, receiver.account)
|
||||
if (res instanceof Promise) {
|
||||
res = await res
|
||||
}
|
||||
@@ -225,7 +248,7 @@ export async function isShouldNotifyTx (
|
||||
}
|
||||
}
|
||||
for (const provider of providers) {
|
||||
const allowed = isAllowed(control, personIds, type, provider, notificationControl)
|
||||
const allowed = isAllowed(control, receiver.socialIds, type, provider, notificationControl)
|
||||
|
||||
if (allowed) {
|
||||
const cur = result.get(provider._id) ?? []
|
||||
|
||||
@@ -55,7 +55,8 @@ export type TypeMatchFunc = Resource<
|
||||
person: Ref<Person>,
|
||||
socialIds: PersonId[],
|
||||
type: NotificationType,
|
||||
control: TriggerControl
|
||||
control: TriggerControl,
|
||||
account: AccountUuid
|
||||
) => boolean | Promise<boolean>
|
||||
>
|
||||
|
||||
@@ -123,6 +124,7 @@ export default plugin(serverNotificationId, {
|
||||
PushNotificationsHandler: '' as Resource<TriggerFunc>
|
||||
},
|
||||
function: {
|
||||
IsUserEmployeeInFieldValueTypeMatch: '' as TypeMatchFunc
|
||||
IsUserEmployeeInFieldValueTypeMatch: '' as TypeMatchFunc,
|
||||
MentionTypeMatch: '' as TypeMatchFunc
|
||||
}
|
||||
})
|
||||
|
||||
@@ -307,16 +307,7 @@ export async function OnToDoCreate (txes: TxCUD<Doc>[], control: TriggerControl)
|
||||
|
||||
const senderInfo: SenderInfo = await getSenderInfo(control.ctx, tx.modifiedBy, control)
|
||||
const notificationControl = await getNotificationProviderControl(control.ctx, control)
|
||||
const notifyResult = await isShouldNotifyTx(
|
||||
control,
|
||||
createTx,
|
||||
todo,
|
||||
employee._id,
|
||||
socialIds,
|
||||
true,
|
||||
false,
|
||||
notificationControl
|
||||
)
|
||||
const notifyResult = await isShouldNotifyTx(control, createTx, todo, receiverInfo, true, false, notificationControl)
|
||||
const content = await getNotificationContent(tx, employee._id, senderInfo, todo, control)
|
||||
const data: Partial<Data<CommonInboxNotification>> = {
|
||||
...content,
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
type Tx,
|
||||
type TxFactory,
|
||||
type TxResult,
|
||||
type UserStatus,
|
||||
type WorkspaceIds,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
@@ -205,6 +206,7 @@ export interface PipelineContext {
|
||||
|
||||
broadcastEvent?: (ctx: MeasureContext, tx: Tx[]) => Promise<void>
|
||||
communicationApi: CommunicationApi | null
|
||||
userStatusMap?: Map<Ref<UserStatus>, { online: boolean, user: AccountUuid }>
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
@@ -269,6 +271,7 @@ export interface TriggerControl {
|
||||
lowLevel: LowLevelStorage
|
||||
modelDb: ModelDb
|
||||
removedMap: Map<Ref<Doc>, Doc>
|
||||
userStatusMap: Map<Ref<UserStatus>, { online: boolean, user: AccountUuid }>
|
||||
|
||||
queue?: PlatformQueue
|
||||
|
||||
|
||||
@@ -37,3 +37,4 @@ export * from './txPush'
|
||||
export * from './queue'
|
||||
export * from './identity'
|
||||
export * from './pluginConfig'
|
||||
export * from './userStatus'
|
||||
|
||||
@@ -139,6 +139,7 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
|
||||
hierarchy: this.context.hierarchy,
|
||||
cache: this.cache,
|
||||
communicationApi: this.context.communicationApi,
|
||||
userStatusMap: this.context.userStatusMap ?? new Map(),
|
||||
apply: async (ctx, tx, needResult) => {
|
||||
if (needResult === true) {
|
||||
return (await this.context.derived?.tx(ctx, tx)) ?? {}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Copyright © 2025 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.
|
||||
//
|
||||
import core, {
|
||||
MeasureContext,
|
||||
Tx,
|
||||
type SessionData,
|
||||
type TxApplyIf,
|
||||
TxCUD,
|
||||
Doc,
|
||||
TxProcessor,
|
||||
UserStatus,
|
||||
TxCreateDoc,
|
||||
TxUpdateDoc,
|
||||
Ref
|
||||
} from '@hcengineering/core'
|
||||
import { BaseMiddleware, Middleware, TxMiddlewareResult, type PipelineContext } from '@hcengineering/server-core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class UserStatusMiddleware extends BaseMiddleware implements Middleware {
|
||||
private constructor (context: PipelineContext, next?: Middleware) {
|
||||
super(context, next)
|
||||
}
|
||||
|
||||
static async create (
|
||||
_: MeasureContext,
|
||||
context: PipelineContext,
|
||||
next: Middleware | undefined
|
||||
): Promise<UserStatusMiddleware> {
|
||||
return new UserStatusMiddleware(context, next)
|
||||
}
|
||||
|
||||
tx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxMiddlewareResult> {
|
||||
for (const tx of txes) {
|
||||
if (tx._class === core.class.TxApplyIf) {
|
||||
const atx = tx as TxApplyIf
|
||||
atx.txes.forEach((it) => {
|
||||
this.processTx(it)
|
||||
})
|
||||
} else {
|
||||
this.processTx(tx as TxCUD<Doc>)
|
||||
}
|
||||
}
|
||||
return this.provideTx(ctx, txes)
|
||||
}
|
||||
|
||||
private processTx (tx: TxCUD<Doc>): void {
|
||||
if (tx._class === core.class.TxCreateDoc && tx.objectClass === core.class.UserStatus) {
|
||||
const status = TxProcessor.createDoc2Doc(tx as TxCreateDoc<UserStatus>)
|
||||
const map = this.context.userStatusMap ?? new Map()
|
||||
map.set(status._id, { online: status.online, user: status.user })
|
||||
this.context.userStatusMap = map
|
||||
} else if (tx._class === core.class.TxUpdateDoc && tx.objectClass === core.class.UserStatus) {
|
||||
const uTx = tx as TxUpdateDoc<UserStatus>
|
||||
if ('online' in uTx.operations) {
|
||||
const current = this.context.userStatusMap?.get(uTx.objectId)
|
||||
const online = uTx.operations.online
|
||||
if (current !== undefined && online !== undefined) {
|
||||
this.context.userStatusMap?.set(uTx.objectId, { online, user: current.user })
|
||||
}
|
||||
}
|
||||
} else if (tx._class === core.class.TxRemoveDoc && tx.objectClass === core.class.UserStatus) {
|
||||
this.context.userStatusMap?.delete(tx.objectId as Ref<UserStatus>)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,8 @@ import {
|
||||
SpacePermissionsMiddleware,
|
||||
SpaceSecurityMiddleware,
|
||||
TriggersMiddleware,
|
||||
TxMiddleware
|
||||
TxMiddleware,
|
||||
UserStatusMiddleware
|
||||
} from '@hcengineering/middleware'
|
||||
import {
|
||||
createBenchmarkAdapter,
|
||||
@@ -127,6 +128,7 @@ export function createServerPipeline (
|
||||
ConfigurationMiddleware.create,
|
||||
ContextNameMiddleware.create,
|
||||
MarkDerivedEntryMiddleware.create,
|
||||
UserStatusMiddleware.create,
|
||||
ApplyTxMiddleware.create, // Extract apply
|
||||
TxMiddleware.create, // Store tx into transaction domain
|
||||
...(opt.disableTriggers === true ? [] : [TriggersMiddleware.create]),
|
||||
|
||||
Reference in New Issue
Block a user