UBER-30 Notify user when added to collaborators (#3200)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-05-17 11:18:05 +06:00 committed by GitHub
parent 72f1570b7f
commit 4949142b19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 254 additions and 44 deletions

View File

@ -32,6 +32,7 @@
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-preference": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/view": "^0.6.6",
"@hcengineering/workbench": "^0.6.6",
"@hcengineering/model-workbench": "^0.6.1",

View File

@ -52,6 +52,7 @@ import type { Asset, IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
import notification from './plugin'
import activity from '@hcengineering/activity'
export { notificationId } from '@hcengineering/notification'
export { notificationOperation } from './migration'
@ -301,6 +302,48 @@ export function createModel (builder: Builder): void {
mode: ['workbench', 'browser', 'editor', 'panel', 'popup']
}
})
builder.createDoc(
notification.class.NotificationGroup,
core.space.Model,
{
label: notification.string.Notifications,
icon: notification.icon.Notifications
},
notification.ids.NotificationGroup
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
generated: false,
label: notification.string.Collaborators,
group: notification.ids.NotificationGroup,
txClasses: [],
objectClass: notification.mixin.Collaborators,
providers: {
[notification.providers.PlatformNotification]: true
}
},
notification.ids.CollaboratoAddNotification
)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: notification.mixin.Collaborators,
icon: notification.icon.Notifications,
txClass: core.class.TxMixin,
component: notification.activity.TxCollaboratorsChange,
display: 'inline',
editable: false,
hideOnRemove: true
},
notification.ids.TxCollaboratorsChange
)
}
export function generateClassNotificationTypes (

View File

@ -20,6 +20,7 @@ import { IntlString, Resource, mergeIds } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
import { Action, ActionCategory, ViewAction } from '@hcengineering/view'
import { Application } from '@hcengineering/workbench'
import { TxViewlet } from '@hcengineering/activity'
export default mergeIds(notificationId, notification, {
string: {
@ -33,6 +34,12 @@ export default mergeIds(notificationId, notification, {
app: {
Notification: '' as Ref<Application>
},
activity: {
TxCollaboratorsChange: '' as AnyComponent
},
ids: {
TxCollaboratorsChange: '' as Ref<TxViewlet>
},
component: {
NotificationSettings: '' as AnyComponent
},

View File

@ -351,13 +351,13 @@
</div>
{:else if hasMessageType && model.length > 0 && (tx.updateTx || tx.mixinTx)}
{#await getValue(client, model[0], tx) then value}
{@const compareValue = getPrevValue(client, model[0], tx)}
{@const prevValue = getPrevValue(client, model[0], tx)}
<div class="activity-content content" class:indent={isAttached} class:contentHidden>
<ShowMore ignore={edit || compareValue !== undefined}>
<ShowMore ignore={edit || prevValue !== undefined}>
{#if value.isObjectSet}
<ObjectPresenter value={value.set} inline />
{:else if showDiff}
<svelte:component this={model[0].presenter} value={value.set} inline {compareValue} showOnlyDiff />
<svelte:component this={model[0].presenter} value={value.set} inline {prevValue} showOnlyDiff />
{/if}
</ShowMore>
</div>

View File

@ -77,7 +77,7 @@ async function createPseudoViewlet (
}
export function getDTxProps (dtx: DisplayTx): any {
return { tx: dtx.tx, value: dtx.doc, isOwnTx: dtx.isOwnTx }
return { tx: dtx.tx, value: dtx.doc, isOwnTx: dtx.isOwnTx, prevValue: dtx.prevDoc }
}
function getViewlet (viewlets: Map<ActivityKey, TxViewlet>, dtx: DisplayTx): TxDisplayViewlet | undefined {

View File

@ -17,6 +17,8 @@
"Inbox": "Inbox",
"Collaborators": "Collaborators",
"Change": "Change",
"AddedRemoved": "Added/removed"
"AddedRemoved": "Added/removed",
"YouAddedCollaborators": "You was added to collaborators",
"ChangeCollaborators": "changed collaborators"
}
}

View File

@ -17,6 +17,8 @@
"Inbox": "Входящие",
"Collaborators": "Участники",
"Change": "Изменено",
"AddedRemoved": "Добавлено/удалено"
"AddedRemoved": "Добавлено/удалено",
"YouAddedCollaborators": "Вы были добавлены как участник",
"ChangeCollaborators": "изменил(а) участники"
}
}

View File

@ -0,0 +1,79 @@
<!--
// 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 { EmployeeAccount } from '@hcengineering/contact'
import { EmployeeAccountRefPresenter } from '@hcengineering/contact-resources'
import { Doc, Ref, TxMixin } from '@hcengineering/core'
import { Collaborators } from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
import { IconAdd, IconDelete, Label } from '@hcengineering/ui'
import notification from '../../plugin'
export let tx: TxMixin<Doc, Collaborators>
export let value: Collaborators
export let prevValue: Collaborators | undefined = undefined
interface Diff {
added: Ref<EmployeeAccount>[]
removed: Ref<EmployeeAccount>[]
}
const client = getClient()
const hierarchy = client.getHierarchy()
function buildDiff (value: Collaborators, prev: Collaborators | undefined): Diff | undefined {
if (prev === undefined) return
const added: Ref<EmployeeAccount>[] = []
const removed: Ref<EmployeeAccount>[] = []
const mixin = hierarchy.as(value, notification.mixin.Collaborators)
const prevMixin = hierarchy.as(prev, notification.mixin.Collaborators)
const prevSet = new Set(prevMixin?.collaborators ?? [])
const newSet = new Set(mixin.collaborators)
for (const newCollab of mixin.collaborators) {
if (!prevSet.has(newCollab)) added.push(newCollab as Ref<EmployeeAccount>)
}
for (const oldCollab of prevMixin?.collaborators ?? []) {
if (!newSet.has(oldCollab)) removed.push(oldCollab as Ref<EmployeeAccount>)
}
return {
added,
removed
}
}
$: diff = buildDiff(value, prevValue)
</script>
{#if diff}
<div class="flex-presenter">
<Label label={notification.string.ChangeCollaborators} />
{#if diff.added.length > 0}
<IconAdd size={'x-small'} fill={'var(--theme-trans-color)'} />
{#each diff.added as add}
<EmployeeAccountRefPresenter value={add} disabled />
{/each}
{/if}
{#if diff.removed.length > 0}
<IconDelete size={'x-small'} fill={'var(--theme-trans-color)'} />
{#each diff.removed as removed}
<EmployeeAccountRefPresenter value={removed} disabled />
{/each}
{/if}
</div>
{:else}
<Label label={notification.string.YouAddedCollaborators} />
{/if}

View File

@ -18,6 +18,7 @@ import { Resources } from '@hcengineering/platform'
import Inbox from './components/Inbox.svelte'
import NotificationSettings from './components/NotificationSettings.svelte'
import NotificationPresenter from './components/NotificationPresenter.svelte'
import TxCollaboratorsChange from './components/activity/TxCollaboratorsChange.svelte'
import { NotificationClientImpl, hasntNotifications, hide, markAsUnread, unsubscribe } from './utils'
export * from './utils'
@ -30,6 +31,9 @@ export default async (): Promise<Resources> => ({
NotificationPresenter,
NotificationSettings
},
activity: {
TxCollaboratorsChange
},
function: {
GetNotificationClient: NotificationClientImpl.getClient,
HasntNotifications: hasntNotifications

View File

@ -28,6 +28,8 @@ export default mergeIds(notificationId, notification, {
MarkAsRead: '' as IntlString,
MarkAllAsRead: '' as IntlString,
Change: '' as IntlString,
AddedRemoved: '' as IntlString
AddedRemoved: '' as IntlString,
YouAddedCollaborators: '' as IntlString,
ChangeCollaborators: '' as IntlString
}
})

View File

@ -202,7 +202,9 @@ const notification = plugin(notificationId, {
NotificationGroup: '' as Ref<Class<NotificationGroup>>
},
ids: {
NotificationSettings: '' as Ref<Doc>
NotificationSettings: '' as Ref<Doc>,
NotificationGroup: '' as Ref<NotificationGroup>,
CollaboratoAddNotification: '' as Ref<NotificationType>
},
providers: {
PlatformNotification: '' as Ref<NotificationProvider>,

View File

@ -18,7 +18,7 @@
import { ShowMore } from '@hcengineering/ui'
export let value: string | undefined
export let compareValue: string | undefined = undefined
export let prevValue: string | undefined = undefined
export let showOnlyDiff: boolean = false
function removeSimilarLines (str1: string | undefined, str2: string | undefined) {
@ -35,14 +35,14 @@
}
}
value = result1
compareValue = result2
prevValue = result2
}
$: showOnlyDiff && removeSimilarLines(value, compareValue)
$: showOnlyDiff && removeSimilarLines(value, prevValue)
</script>
<ShowMore>
{#key [value, compareValue]}
<CollaborationDiffViewer content={value ?? ''} comparedVersion={compareValue ?? ''} noButton readonly />
{#key [value, prevValue]}
<CollaborationDiffViewer content={value ?? ''} comparedVersion={prevValue ?? ''} noButton readonly />
{/key}
</ShowMore>

View File

@ -421,6 +421,45 @@ async function isShouldNotify (
}
}
function pushNotification (
control: TriggerControl,
res: Tx[],
target: Ref<Account>,
object: Doc,
originTx: TxCUD<Doc>,
docUpdates: DocUpdates[]
): void {
const current = docUpdates.find((p) => p.user === target)
if (current === undefined) {
res.push(
control.txFactory.createTxCreateDoc(notification.class.DocUpdates, object.space, {
user: target,
attachedTo: object._id,
attachedToClass: object._class,
hidden: false,
lastTx: originTx._id,
lastTxTime: originTx.modifiedOn,
txes: [[originTx._id, originTx.modifiedOn]]
})
)
} else {
res.push(
control.txFactory.createTxUpdateDoc(current._class, current.space, current._id, {
$push: {
txes: [originTx._id, originTx.modifiedOn]
}
})
)
res.push(
control.txFactory.createTxUpdateDoc(current._class, current.space, current._id, {
lastTx: originTx._id,
lastTxTime: originTx.modifiedOn,
hidden: false
})
)
}
}
async function getNotificationTxes (
control: TriggerControl,
object: Doc,
@ -432,35 +471,7 @@ async function getNotificationTxes (
const res: Tx[] = []
const allowed = await isShouldNotify(control, originTx, object, target, isSpace)
if (allowed.allowed) {
const current = docUpdates.find((p) => p.user === target)
if (current === undefined) {
res.push(
control.txFactory.createTxCreateDoc(notification.class.DocUpdates, object.space, {
user: target,
attachedTo: object._id,
attachedToClass: object._class,
hidden: false,
lastTx: originTx._id,
lastTxTime: originTx.modifiedOn,
txes: [[originTx._id, originTx.modifiedOn]]
})
)
} else {
res.push(
control.txFactory.createTxUpdateDoc(current._class, current.space, current._id, {
$push: {
txes: [originTx._id, originTx.modifiedOn]
}
})
)
res.push(
control.txFactory.createTxUpdateDoc(current._class, current.space, current._id, {
lastTx: originTx._id,
lastTxTime: originTx.modifiedOn,
hidden: false
})
)
}
pushNotification(control, res, target, object, originTx, docUpdates)
}
for (const type of allowed.emails) {
const emailTx = await createEmailNotificationTxes(
@ -572,9 +583,12 @@ export async function collaboratorDocHandler (
if (tx.space === core.space.DerivedTx) return []
return await createCollaboratorDoc(tx as TxCreateDoc<Doc>, control, originTx ?? tx)
case core.class.TxUpdateDoc:
case core.class.TxMixin:
case core.class.TxMixin: {
if (tx.space === core.space.DerivedTx) return []
return await updateCollaboratorDoc(tx as TxUpdateDoc<Doc>, control, originTx ?? tx)
let res = await updateCollaboratorDoc(tx as TxUpdateDoc<Doc>, control, originTx ?? tx)
res = res.concat(await updateCollaboratorsMixin(tx as TxMixin<Doc, Collaborators>, control, originTx ?? tx))
return res
}
case core.class.TxRemoveDoc:
return await removeCollaboratorDoc(tx as TxRemoveDoc<Doc>, control)
case core.class.TxCollectionCUD:
@ -584,6 +598,60 @@ export async function collaboratorDocHandler (
return []
}
async function updateCollaboratorsMixin (
tx: TxMixin<Doc, Collaborators>,
control: TriggerControl,
originTx: TxCUD<Doc>
): Promise<Tx[]> {
if (tx._class !== core.class.TxMixin) return []
if (!control.hierarchy.isDerived(tx.mixin, notification.mixin.Collaborators)) return []
const res: Tx[] = []
if (tx.attributes.collaborators !== undefined) {
const createTx = control.hierarchy.isDerived(tx.objectClass, core.class.AttachedDoc)
? (
await control.findAll(core.class.TxCollectionCUD, {
'tx.objectId': tx.objectId,
'tx._class': core.class.TxCreateDoc
})
)[0]
: (
await control.findAll(core.class.TxCreateDoc, {
objectId: tx.objectId
})
)[0]
const mixinTxes = await control.findAll(core.class.TxMixin, {
objectId: tx.objectId
})
const prevDoc = TxProcessor.buildDoc2Doc([createTx, ...mixinTxes]) as Collaborators
const set = new Set(prevDoc?.collaborators ?? [])
const newCollabs: Ref<Account>[] = []
for (const collab of tx.attributes.collaborators) {
if (!set.has(collab)) {
if (
await isAllowed(
control,
collab as Ref<EmployeeAccount>,
notification.ids.CollaboratoAddNotification,
notification.providers.PlatformNotification
)
) {
newCollabs.push(collab)
}
}
}
if (newCollabs.length > 0) {
const docUpdates = await control.findAll(notification.class.DocUpdates, {
user: { $in: newCollabs },
attachedTo: tx.objectId
})
for (const collab of newCollabs) {
pushNotification(control, res, collab, prevDoc, originTx, docUpdates)
}
}
}
return res
}
async function collectionCollabDoc (tx: TxCollectionCUD<Doc, AttachedDoc>, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
let res = await collaboratorDocHandler(actualTx as TxCUD<Doc>, control, tx)