UBERF-6124: Rework inbox view (#5046)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-03-25 07:55:14 +04:00 committed by GitHub
parent 9f198a5428
commit 3bbb8915ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
108 changed files with 2424 additions and 2010 deletions

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,8 @@ import {
type ActivityMessageControl,
type SavedMessage,
type IgnoreActivity,
type ActivityReference
type ActivityReference,
type ActivityMessagePreview
} from '@hcengineering/activity'
import core, {
DOMAIN_MODEL,
@ -257,6 +258,11 @@ export class TSavedMessage extends TPreference implements SavedMessage {
declare attachedTo: Ref<ActivityMessage>
}
@Mixin(activity.mixin.ActivityMessagePreview, core.class.Class)
export class TActivityMessagePreview extends TClass implements ActivityMessagePreview {
presenter!: AnyComponent
}
export function createModel (builder: Builder): void {
builder.createModel(
TTxViewlet,
@ -273,7 +279,8 @@ export function createModel (builder: Builder): void {
TActivityMessageControl,
TSavedMessage,
TIgnoreActivity,
TActivityReference
TActivityReference,
TActivityMessagePreview
)
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, view.mixin.ObjectPresenter, {
@ -288,6 +295,18 @@ export function createModel (builder: Builder): void {
presenter: activity.component.ActivityReferencePresenter
})
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, activity.mixin.ActivityMessagePreview, {
presenter: activity.component.DocUpdateMessagePreview
})
builder.mixin(activity.class.ActivityInfoMessage, core.class.Class, activity.mixin.ActivityMessagePreview, {
presenter: activity.component.ActivityInfoMessagePreview
})
builder.mixin(activity.class.ActivityReference, core.class.Class, activity.mixin.ActivityMessagePreview, {
presenter: activity.component.ActivityReferencePreview
})
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, view.mixin.LinkProvider, {
encode: activity.function.GetFragment
})
@ -350,14 +369,6 @@ export function createModel (builder: Builder): void {
labelPresenter: activity.component.ActivityMessageNotificationLabel
})
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
messageMatch: {
_class: activity.class.DocUpdateMessage,
objectClass: activity.class.Reaction
},
presenter: activity.component.ReactionNotificationPresenter
})
builder.createDoc(
notification.class.NotificationType,
core.space.Model,

View File

@ -138,7 +138,7 @@ export class TDirectMessageInput extends TClass implements DirectMessageInput {
}
@Model(chunter.class.ChatMessage, activity.class.ActivityMessage)
@UX(chunter.string.Message)
@UX(chunter.string.Message, chunter.icon.Thread, undefined, undefined, undefined, chunter.string.Threads)
export class TChatMessage extends TActivityMessage implements ChatMessage {
@Prop(TypeMarkup(), chunter.string.Message)
@Index(IndexKind.FullText)
@ -154,7 +154,7 @@ export class TChatMessage extends TActivityMessage implements ChatMessage {
}
@Model(chunter.class.ThreadMessage, chunter.class.ChatMessage)
@UX(chunter.string.ThreadMessage)
@UX(chunter.string.ThreadMessage, chunter.icon.Thread, undefined, undefined, undefined, chunter.string.Threads)
export class TThreadMessage extends TChatMessage implements ThreadMessage {
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedTo)
@Index(IndexKind.Indexed)
@ -403,28 +403,29 @@ export function createModel (builder: Builder, options = { addApplication: true
encode: chunter.function.GetThreadLink
})
createAction(
builder,
{
action: view.actionImpl.CopyTextToClipboard,
actionProps: {
textProvider: chunter.function.GetLink
},
label: chunter.string.CopyLink,
icon: chunter.icon.Copy,
keyBinding: [],
input: 'none',
category: chunter.category.Chunter,
target: activity.class.ActivityMessage,
visibilityTester: chunter.function.CanCopyMessageLink,
context: {
mode: ['context', 'browser'],
application: chunter.app.Chunter,
group: 'copy'
}
},
chunter.action.CopyChatMessageLink
)
// Note: it is not working now, need to fix navigation by url UBERF-5686
// createAction(
// builder,
// {
// action: view.actionImpl.CopyTextToClipboard,
// actionProps: {
// textProvider: chunter.function.GetLink
// },
// label: chunter.string.CopyLink,
// icon: chunter.icon.Copy,
// keyBinding: [],
// input: 'none',
// category: chunter.category.Chunter,
// target: activity.class.ActivityMessage,
// visibilityTester: chunter.function.CanCopyMessageLink,
// context: {
// mode: ['context', 'browser'],
// application: chunter.app.Chunter,
// group: 'copy'
// }
// },
// chunter.action.CopyChatMessageLink
// )
builder.mixin(chunter.class.ChunterMessage, core.class.Class, view.mixin.ClassFilters, {
filters: ['space', '_class']
@ -732,6 +733,14 @@ export function createModel (builder: Builder, options = { addApplication: true
builder.mixin(chunter.class.DirectMessage, core.class.Class, chunter.mixin.ObjectChatPanel, {
ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description']
})
builder.mixin(chunter.class.ChatMessage, core.class.Class, activity.mixin.ActivityMessagePreview, {
presenter: chunter.component.ChatMessagePreview
})
builder.mixin(chunter.class.ThreadMessage, core.class.Class, activity.mixin.ActivityMessagePreview, {
presenter: chunter.component.ThreadMessagePreview
})
}
export default chunter

View File

@ -79,7 +79,6 @@ import setting from '@hcengineering/setting'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import notification from './plugin'
import { defineViewlets } from './viewlets'
export { notificationId } from '@hcengineering/notification'
export { notificationOperation } from './migration'
@ -256,9 +255,17 @@ export class TCommonInboxNotification extends TInboxNotification implements Comm
@Prop(TypeIntlString(), core.string.String)
header?: IntlString
@Prop(TypeRef(core.class.Doc), core.string.Object)
headerObjectId?: Ref<Doc>
@Prop(TypeRef(core.class.Doc), core.string.Class)
headerObjectClass?: Ref<Class<Doc>>
@Prop(TypeIntlString(), notification.string.Message)
message?: IntlString
headerIcon?: Asset
@Prop(TypeString(), notification.string.Message)
messageHtml?: string
@ -452,6 +459,14 @@ export function createModel (builder: Builder): void {
notification.ids.TxDmCreation
)
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
presenter: notification.component.NotificationCollaboratorsChanged,
messageMatch: {
_class: activity.class.DocUpdateMessage,
'attributeUpdates.attrClass': notification.mixin.Collaborators
}
})
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,
@ -461,58 +476,13 @@ export function createModel (builder: Builder): void {
icon: notification.icon.Notifications,
label: notification.string.ChangeCollaborators
},
notification.ids.NotificationCollaboratorsChanged
notification.ids.CollaboratorsChangedMessage
)
builder.mixin(notification.mixin.Collaborators, core.class.Class, activity.mixin.ActivityAttributeUpdatesPresenter, {
presenter: notification.component.NotificationCollaboratorsChanged
presenter: notification.component.CollaboratorsChanged
})
createAction(
builder,
{
action: notification.actionImpl.MarkAsReadInboxNotification,
label: notification.string.MarkAsRead,
icon: notification.icon.Notifications,
input: 'focus',
visibilityTester: notification.function.HasMarkAsReadAction,
category: notification.category.Notification,
target: notification.class.InboxNotification,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.MarkAsReadInboxNotification
)
createAction(
builder,
{
action: notification.actionImpl.MarkAsUnreadInboxNotification,
label: notification.string.MarkAsUnread,
icon: notification.icon.Track,
input: 'focus',
visibilityTester: notification.function.HasMarkAsUnreadAction,
category: notification.category.Notification,
target: notification.class.InboxNotification,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.MarkAsUnreadInboxNotification
)
createAction(
builder,
{
action: notification.actionImpl.DeleteInboxNotification,
label: notification.string.Archive,
icon: view.icon.Archive,
input: 'focus',
keyBinding: ['Backspace'],
category: notification.category.Notification,
target: notification.class.InboxNotification,
context: { mode: ['context', 'browser'], group: 'edit' }
},
notification.action.DeleteInboxNotification
)
createAction(
builder,
{
@ -523,7 +493,7 @@ export function createModel (builder: Builder): void {
visibilityTester: notification.function.CanReadNotifyContext,
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
context: { mode: ['context', 'panel'], application: notification.app.Notification, group: 'edit' }
},
notification.action.ReadNotifyContext
)
@ -538,7 +508,7 @@ export function createModel (builder: Builder): void {
visibilityTester: notification.function.CanUnReadNotifyContext,
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
context: { mode: ['context', 'panel'], application: notification.app.Notification, group: 'edit' }
},
notification.action.UnReadNotifyContext
)
@ -552,7 +522,7 @@ export function createModel (builder: Builder): void {
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
context: { mode: ['panel'], application: notification.app.Notification, group: 'remove' }
},
notification.action.DeleteContextNotifications
)
@ -560,37 +530,18 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: notification.actionImpl.HideDocNotifyContext,
label: notification.string.DontTrack,
icon: notification.icon.DontTrack,
action: notification.actionImpl.Unsubscribe,
label: notification.string.Unsubscribe,
icon: view.icon.EyeCrossed,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: {
mode: ['browser', 'context'],
mode: ['panel'],
group: 'remove'
},
visibilityTester: notification.function.IsDocNotifyContextTracked
}
},
notification.action.HideDocNotifyContext
)
createAction(
builder,
{
action: notification.actionImpl.UnHideDocNotifyContext,
label: view.string.UnArchive,
icon: view.icon.Archive,
input: 'focus',
category: view.category.General,
target: notification.class.DocNotifyContext,
context: {
mode: ['browser', 'context'],
group: 'remove'
},
visibilityTester: notification.function.IsDocNotifyContextHidden
},
notification.action.UnHideDocNotifyContext
notification.action.Unsubscribe
)
builder.mixin(notification.class.DocNotifyContext, core.class.Class, view.mixin.ObjectPresenter, {
@ -605,8 +556,6 @@ export function createModel (builder: Builder): void {
presenter: notification.component.CommonInboxNotificationPresenter
})
defineViewlets(builder)
builder.createDoc(
notification.class.CommonNotificationType,
core.space.Model,
@ -640,7 +589,7 @@ export function createModel (builder: Builder): void {
target: core.class.Doc,
context: {
mode: ['browser'],
group: 'edit'
group: 'remove'
}
},
notification.action.ArchiveAll
@ -688,6 +637,14 @@ export function createModel (builder: Builder): void {
{ label: notification.string.Inbox, visible: true },
notification.category.Notification
)
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
messageMatch: {
_class: activity.class.DocUpdateMessage,
objectClass: activity.class.Reaction
},
presenter: notification.component.ReactionNotificationPresenter
})
}
export function generateClassNotificationTypes (

View File

@ -34,7 +34,8 @@ export default mergeIds(notificationId, notification, {
ChangeCollaborators: '' as IntlString,
Message: '' as IntlString,
StarDocument: '' as IntlString,
UnstarDocument: '' as IntlString
UnstarDocument: '' as IntlString,
Unsubscribe: '' as IntlString
},
app: {
Notification: '' as Ref<Application>,
@ -46,7 +47,7 @@ export default mergeIds(notificationId, notification, {
ids: {
TxCollaboratorsChange: '' as Ref<TxViewlet>,
TxDmCreation: '' as Ref<TxViewlet>,
NotificationCollaboratorsChanged: '' as Ref<DocUpdateMessageViewlet>
CollaboratorsChangedMessage: '' as Ref<DocUpdateMessageViewlet>
},
component: {
NotificationSettings: '' as AnyComponent,
@ -54,8 +55,6 @@ export default mergeIds(notificationId, notification, {
CommonInboxNotificationPresenter: '' as AnyComponent
},
function: {
HasMarkAsUnreadAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasMarkAsReadAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasDocNotifyContextPinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasDocNotifyContextUnpinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanReadNotifyContext: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
@ -73,13 +72,8 @@ export default mergeIds(notificationId, notification, {
},
actionImpl: {
Unsubscribe: '' as ViewAction,
MarkAsUnreadInboxNotification: '' as ViewAction,
MarkAsReadInboxNotification: '' as ViewAction,
DeleteInboxNotification: '' as ViewAction,
UnpinDocNotifyContext: '' as ViewAction,
PinDocNotifyContext: '' as ViewAction,
HideDocNotifyContext: '' as ViewAction,
UnHideDocNotifyContext: '' as ViewAction,
UnReadNotifyContext: '' as ViewAction,
ReadNotifyContext: '' as ViewAction,
DeleteContextNotifications: '' as ViewAction,

View File

@ -1,64 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { Builder } from '@hcengineering/model'
import view from '@hcengineering/model-view'
import core from '@hcengineering/model-core'
import notification from '@hcengineering/notification'
export function defineViewlets (builder: Builder): void {
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: notification.string.GroupedList,
icon: view.icon.Card,
component: notification.component.InboxGroupedListView
},
notification.viewlet.GroupedList
)
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: notification.string.FlatList,
icon: view.icon.List,
component: notification.component.InboxFlatListView
},
notification.viewlet.FlatList
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: notification.class.DocNotifyContext,
descriptor: notification.viewlet.FlatList,
config: []
},
notification.viewlet.InboxFlatList
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: notification.class.DocNotifyContext,
descriptor: notification.viewlet.GroupedList,
config: []
},
notification.viewlet.InboxGroupedList
)
}

View File

@ -35,7 +35,6 @@ export default mergeIds(timeId, time, {
GotoTimePlaning: '' as IntlString,
GotoTimeTeamPlaning: '' as IntlString,
NewToDo: '' as IntlString,
ToDo: '' as IntlString,
Priority: '' as IntlString,
MarkedAsDone: '' as IntlString
},

View File

@ -706,4 +706,8 @@ export function createModel (builder: Builder): void {
},
tracker.descriptors.Issue
)
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectIcon, {
component: tracker.component.IssueStatusPresenter
})
}

View File

@ -17,6 +17,7 @@
import Nodes from './message/Nodes.svelte'
export let message: string
export let preview = false
let dom: HTMLElement
@ -30,4 +31,4 @@
$: dom = doc.firstChild?.childNodes[1] as HTMLElement
</script>
<Nodes nodes={dom.childNodes} />
<Nodes nodes={dom.childNodes} {preview} />

View File

@ -22,6 +22,7 @@
import ObjectNode from './ObjectNode.svelte'
export let nodes: NodeListOf<any>
export let preview = false
function prevName (pos: number, nodes: NodeListOf<any>): string | undefined {
while (true) {
@ -61,23 +62,26 @@
{#if node.nodeType === Node.TEXT_NODE}
{node.data}
{:else if node.nodeName === 'EM'}
<em><svelte:self nodes={node.childNodes} /></em>
<em><svelte:self nodes={node.childNodes} {preview} /></em>
{:else if node.nodeName === 'STRONG' || node.nodeName === 'B'}
<strong><svelte:self nodes={node.childNodes} /></strong>
<strong><svelte:self nodes={node.childNodes} {preview} /></strong>
{:else if node.nodeName === 'U'}
<u><svelte:self nodes={node.childNodes} /></u>
<u><svelte:self nodes={node.childNodes} {preview} /></u>
{:else if node.nodeName === 'P'}
{#if node.childNodes.length > 0}
<p class="p-inline contrast">
<p class="p-inline contrast" class:overflow-label={preview}>
<svelte:self nodes={node.childNodes} />
</p>
{/if}
{:else if node.nodeName === 'BLOCKQUOTE'}
<blockquote><svelte:self nodes={node.childNodes} /></blockquote>
<blockquote style:margin={preview ? '0' : null}><svelte:self nodes={node.childNodes} {preview} /></blockquote>
{:else if node.nodeName === 'CODE'}
<pre class="proseCode"><svelte:self nodes={node.childNodes} /></pre>
<pre class="proseCode"><svelte:self nodes={node.childNodes} {preview} /></pre>
{:else if node.nodeName === 'PRE'}
<pre class="proseCodeBlock"><svelte:self nodes={node.childNodes} /></pre>
<pre class="proseCodeBlock" style:margin={preview ? '0' : null}><svelte:self
nodes={node.childNodes}
{preview}
/></pre>
{:else if node.nodeName === 'BR'}
{@const pName = prevName(ni, nodes)}
{#if pName !== 'P' && pName !== 'BR' && pName !== undefined}
@ -88,25 +92,25 @@
{:else if node.nodeName === 'IMG'}
<div class="imgContainer max-h-60 max-w-60">{@html node.outerHTML}</div>
{:else if node.nodeName === 'H1'}
<h1><svelte:self nodes={node.childNodes} /></h1>
<h1><svelte:self nodes={node.childNodes} {preview} /></h1>
{:else if node.nodeName === 'H2'}
<h2><svelte:self nodes={node.childNodes} /></h2>
<h2><svelte:self nodes={node.childNodes} {preview} /></h2>
{:else if node.nodeName === 'H3'}
<h3><svelte:self nodes={node.childNodes} /></h3>
<h3><svelte:self nodes={node.childNodes} {preview} /></h3>
{:else if node.nodeName === 'H4'}
<h4><svelte:self nodes={node.childNodes} /></h4>
<h4><svelte:self nodes={node.childNodes} {preview} /></h4>
{:else if node.nodeName === 'H5'}
<h5><svelte:self nodes={node.childNodes} /></h5>
<h5><svelte:self nodes={node.childNodes} {preview} /></h5>
{:else if node.nodeName === 'H6'}
<h6><svelte:self nodes={node.childNodes} /></h6>
<h6><svelte:self nodes={node.childNodes} {preview} /></h6>
{:else if node.nodeName === 'UL' || node.nodeName === 'LIST'}
<ul><svelte:self nodes={node.childNodes} /></ul>
<ul style:margin={preview ? '0' : null}><svelte:self nodes={node.childNodes} {preview} /></ul>
{:else if node.nodeName === 'OL' || node.nodeName === 'LIST=1'}
<ol><svelte:self nodes={node.childNodes} /></ol>
<ol style:margin={preview ? '0' : null}><svelte:self nodes={node.childNodes} {preview} /></ol>
{:else if node.nodeName === 'LI'}
<li class={node.className}><svelte:self nodes={node.childNodes} /></li>
<li class={node.className}><svelte:self nodes={node.childNodes} {preview} /></li>
{:else if node.nodeName === 'DIV'}
<div><svelte:self nodes={node.childNodes} /></div>
<div><svelte:self nodes={node.childNodes} {preview} /></div>
{:else if node.nodeName === 'A'}
<a
href={node.getAttribute('href')}
@ -115,10 +119,10 @@
handleLink(node, e)
}}
>
<svelte:self nodes={node.childNodes} />
<svelte:self nodes={node.childNodes} {preview} />
</a>
{:else if node.nodeName === 'LABEL'}
<svelte:self nodes={node.childNodes} />
<svelte:self nodes={node.childNodes} {preview} />
{:else if node.nodeName === 'INPUT'}
{#if node.type?.toLowerCase() === 'checkbox'}
<div class="checkboxContainer">
@ -132,23 +136,23 @@
{#if objectClass !== undefined && objectId !== undefined}
<ObjectNode _id={objectId} _class={correctClass(objectClass)} title={node.getAttribute('data-label')} />
{:else}
<svelte:self nodes={node.childNodes} />
<svelte:self nodes={node.childNodes} {preview} />
{/if}
{:else if node.nodeName === 'TABLE'}
<table class={node.className}><svelte:self nodes={node.childNodes} /></table>
<table class={node.className}><svelte:self nodes={node.childNodes} {preview} /></table>
{:else if node.nodeName === 'TBODY'}
<tbody><svelte:self nodes={node.childNodes} /></tbody>
<tbody><svelte:self nodes={node.childNodes} {preview} /></tbody>
{:else if node.nodeName === 'TR'}
<tr><svelte:self nodes={node.childNodes} /></tr>
<tr><svelte:self nodes={node.childNodes} {preview} /></tr>
{:else if node.nodeName === 'TH'}
<th><svelte:self nodes={node.childNodes} /></th>
<th><svelte:self nodes={node.childNodes} {preview} /></th>
{:else if node.nodeName === 'TD'}
<td><svelte:self nodes={node.childNodes} /></td>
<td><svelte:self nodes={node.childNodes} {preview} /></td>
{:else if node.nodeName === 'S'}
<s><svelte:self nodes={node.childNodes} /></s>
<s><svelte:self nodes={node.childNodes} {preview} /></s>
{:else}
unknown: "{node.nodeName}"
<svelte:self nodes={node.childNodes} />
<svelte:self nodes={node.childNodes} {preview} />
{/if}
{/each}
{/if}
@ -184,6 +188,6 @@
li,
.checkboxContainer,
s {
color: var(--theme-accent-color);
color: var(--global-primary-TextColor);
}
</style>

View File

@ -130,7 +130,7 @@ p:last-child { margin-block-end: 0; }
hyphens: auto;
line-height: 150%;
&.contrast { color: var(--theme-caption-color); }
&.contrast { color: var(--global-primary-TextColor); }
&:not(.contrast) { color: var(--theme-content-color); }
}

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import type { Asset } from '@hcengineering/platform'
import { AnySvelteComponent, LabelAndProps } from '../types'
import { AnySvelteComponent, IconSize, LabelAndProps } from '../types'
import { ComponentType } from 'svelte'
import ButtonBase from './ButtonBase.svelte'
@ -22,6 +22,7 @@
export let size: 'large' | 'medium' | 'small' | 'extra-small' | 'min' = 'large'
export let icon: Asset | AnySvelteComponent | ComponentType
export let iconProps: any | undefined = undefined
export let iconSize: IconSize | undefined = undefined
export let disabled: boolean = false
export let pressed: boolean = false
export let hasMenu: boolean = false
@ -41,6 +42,7 @@
bind:this={element}
type={'type-button-icon'}
{kind}
{iconSize}
{size}
{icon}
{iconProps}

View File

@ -22,10 +22,11 @@
export let count: number
export let addClass: string | undefined = undefined
export let noScroll: boolean = false
export let kind: 'default' | 'thin' = 'default'
export let kind: 'default' | 'thin' | 'full-size' = 'default'
export let colorsSchema: 'default' | 'lumia' = 'default'
export let updateOnMouse = true
export let lazy = false
export let highlightIndex: number | undefined = undefined
export let getKey: (index: number) => string = (index) => index.toString()
const refs: HTMLElement[] = []
@ -82,6 +83,7 @@
{addClass}
{row}
{kind}
isHighlighted={row === highlightIndex}
selected={row === selection}
on:click={() => dispatch('click', row)}
on:mouseover={mouseAttractor(() => {
@ -111,6 +113,7 @@
{row}
{kind}
selected={row === selection}
isHighlighted={row === highlightIndex}
on:click={() => dispatch('click', row)}
on:mouseover={mouseAttractor(() => {
if (updateOnMouse) {

View File

@ -18,7 +18,8 @@
export let addClass: string | undefined = undefined
export let selected = false
export let element: HTMLElement | undefined = undefined
export let kind: 'default' | 'thin' = 'default'
export let kind: 'default' | 'thin' | 'full-size' = 'default'
export let isHighlighted = false
</script>
<slot name="category" item={row} />
@ -29,6 +30,7 @@
class:selection={selected}
class:lumia={colorsSchema === 'lumia'}
class:default={colorsSchema === 'default'}
class:highlighted={isHighlighted}
on:mouseover
on:mouseenter
on:focus={() => {}}
@ -53,19 +55,23 @@
}
}
&.full-size {
margin: 0;
}
&.default {
&:hover {
&:hover:not(.highlighted) {
background-color: var(--theme-popup-divider);
}
}
&.lumia {
&:hover {
&:hover:not(.highlighted) {
background-color: var(--global-ui-highlight-BackgroundColor);
}
}
&.selection {
&.selection:not(.highlighted) {
&.default {
background-color: var(--theme-popup-hover);
}
@ -74,5 +80,9 @@
background-color: var(--global-ui-highlight-BackgroundColor);
}
}
&.highlighted {
background-color: var(--global-ui-hover-highlight-BackgroundColor);
}
}
</style>

View File

@ -38,6 +38,8 @@
"Mentioned": "Mentioned",
"You": "You",
"Mentions": "Mentions",
"MentionedYouIn": "Mentioned you in"
"MentionedYouIn": "Mentioned you in",
"Messages": "Messages",
"Thread": "Thread"
}
}

View File

@ -38,6 +38,8 @@
"Mentioned": "Упомянул(а)",
"You": "Вы",
"Mentions": "Упоминания",
"MentionedYouIn": "Упомянул(а) вас в"
"MentionedYouIn": "Упомянул(а) вас в",
"Messages": "Cообщения",
"Thread": "Обсуждение"
}
}

View File

@ -318,7 +318,10 @@ export async function combineActivityMessages (
)
}
export function sortActivityMessages<T extends ActivityMessage> (messages: T[], order: SortingOrder): T[] {
export function sortActivityMessages<T extends ActivityMessage> (
messages: T[],
order: SortingOrder = SortingOrder.Ascending
): T[] {
return messages.sort((message1, message2) =>
order === SortingOrder.Ascending
? activityMessagesComparator(message1, message2)

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { ActionIcon, type AnySvelteComponent } from '@hcengineering/ui'
import { type AnySvelteComponent, ButtonIcon } from '@hcengineering/ui'
import { Asset } from '@hcengineering/platform'
import { ComponentType } from 'svelte'
@ -22,30 +22,12 @@
export let size: 'x-small' | 'small' = 'small'
export let action: (ev: MouseEvent) => Promise<void> | void = async () => {}
export let opened = false
function onClick (ev: MouseEvent): void {
ev.stopPropagation()
ev.preventDefault()
void action(ev)
}
</script>
<div class="action" class:opened>
<ActionIcon {icon} {size} {action} {iconProps} />
</div>
<style lang="scss">
.action {
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
width: 1.5rem;
height: 1.5rem;
padding: 0.25rem;
&:hover {
color: var(--accent-color);
background: var(--global-ui-hover-BackgroundColor);
}
&.opened {
color: var(--accent-color);
background: var(--global-ui-hover-BackgroundColor);
}
}
</style>
<ButtonIcon {icon} {iconProps} iconSize={size} size="small" kind="tertiary" pressed={opened} on:click={onClick} />

View File

@ -69,9 +69,6 @@
<PinMessageAction object={message} />
<SaveMessageAction object={message} />
{/if}
{#if withActionMenu}
<ActivityMessageAction icon={IconMoreV} action={showMenu} opened={isActionMenuOpened} />
{/if}
</div>
{/if}

View File

@ -0,0 +1,250 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { getClient, MessageViewer } from '@hcengineering/presentation'
import { Person, type PersonAccount } from '@hcengineering/contact'
import {
Avatar,
EmployeePresenter,
personAccountByIdStore,
personByIdStore,
SystemAvatar
} from '@hcengineering/contact-resources'
import core, { Account, Doc, Ref, Timestamp } from '@hcengineering/core'
import { Icon, Label, resizeObserver, TimeSince, tooltip } from '@hcengineering/ui'
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import activity, { ActivityMessagePreviewType } from '@hcengineering/activity'
import { classIcon, DocNavLink } from '@hcengineering/view-resources'
export let text: string | undefined = undefined
export let intlLabel: IntlString | undefined = undefined
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
export let timestamp: Timestamp
export let account: Ref<Account> | undefined = undefined
export let isCompact = false
export let headerObject: Doc | undefined = undefined
export let headerIcon: Asset | undefined = undefined
export let header: IntlString | undefined = undefined
const client = getClient()
const limit = 300
let isActionsOpened = false
let person: Person | undefined = undefined
let width: number
$: isCompact = width < limit
$: person = getPerson(account, $personAccountByIdStore, $personByIdStore)
function getPerson (
_id: Ref<Account> | undefined,
accountById: Map<Ref<PersonAccount>, PersonAccount>,
personById: Map<Ref<Person>, Person>
): Person | undefined {
if (_id === undefined) {
return undefined
}
const personAccount = accountById.get(_id as Ref<PersonAccount>)
if (personAccount === undefined) {
return undefined
}
return personById.get(personAccount.person)
}
export function onActionsOpened (): void {
isActionsOpened = true
}
export function onActionsClosed (): void {
isActionsOpened = false
}
let tooltipLabel: IntlString | undefined = undefined
$: if (headerObject !== undefined) {
tooltipLabel = header ?? client.getHierarchy().getClass(headerObject._class).label
} else if (person !== undefined) {
tooltipLabel = getEmbeddedLabel(person.name)
} else {
tooltipLabel = core.string.System
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="root"
class:readonly
class:contentOnly={type === 'content-only'}
class:actionsOpened={isActionsOpened}
use:resizeObserver={(element) => {
width = element.clientWidth
}}
on:click
>
<span class="left overflow-label">
{#if type === 'full'}
<div class="header">
<span class="icon" use:tooltip={{ label: tooltipLabel }}>
{#if headerObject}
<Icon icon={headerIcon ?? classIcon(client, headerObject._class) ?? activity.icon.Activity} size="small" />
{:else if person}
<Avatar size="card" avatar={person.avatar} name={person.name} />
{:else}
<SystemAvatar size="card" />
{/if}
</span>
{#if !isCompact}
{#if headerObject}
<DocNavLink object={headerObject} colorInherit>
<Label label={header ?? client.getHierarchy().getClass(headerObject._class).label} />
</DocNavLink>
{:else if person}
<EmployeePresenter value={person} shouldShowAvatar={false} compact />
{:else}
<Label label={core.string.System} />
{/if}
{/if}
</div>
{/if}
{#if text || intlLabel}
<span class="textContent overflow-label font-normal" class:contentOnly={type === 'content-only'}>
{#if intlLabel}
<Label label={intlLabel} />
{/if}
{#if text}
<MessageViewer message={text} preview />
{/if}
</span>
{/if}
<slot name="content" />
</span>
{#if !readonly}
<div class="actions" class:opened={isActionsOpened}>
<slot name="actions" />
</div>
{/if}
<div class="right">
<slot name="right" />
{#if type === 'full'}
<div class="time">
<TimeSince value={timestamp} />
</div>
{/if}
</div>
</div>
<style lang="scss">
.root {
display: flex;
align-items: center;
justify-content: space-between;
height: 2.375rem;
color: var(--global-primary-TextColor);
width: 100%;
padding: 0 var(--spacing-0_5);
padding-right: var(--spacing-0_75);
padding-left: var(--spacing-1_25);
position: relative;
&.contentOnly {
padding: 0;
height: auto;
}
&.readonly {
cursor: default;
}
.actions {
position: absolute;
visibility: hidden;
top: -1.75rem;
right: 0;
&.opened {
visibility: visible;
}
}
.left {
position: relative;
height: 100%;
display: flex;
align-items: center;
gap: var(--spacing-1);
}
.right {
display: flex;
align-items: center;
gap: var(--spacing-1);
margin-left: var(--spacing-0_5);
}
&:hover:not(.readonly) > .actions {
visibility: visible;
}
&.actionsOpened {
background-color: var(--global-ui-BackgroundColor);
}
}
.header {
display: flex;
align-items: center;
gap: var(--spacing-0_5);
font-weight: 500;
color: var(--global-primary-TextColor);
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.325rem;
max-width: 1.325rem;
min-width: 1.325rem;
}
.time {
white-space: nowrap;
color: var(--global-tertiary-TextColor);
}
.textContent {
display: inline;
overflow: hidden;
max-height: 1.25rem;
color: var(--global-primary-TextColor);
&.contentOnly {
margin-left: 0;
}
}
</style>

View File

@ -0,0 +1,34 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { tooltip } from '@hcengineering/ui'
import { getDisplayTime, Timestamp } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
export let date: Timestamp
$: fullDate = new Date(date).toLocaleString('default', {
minute: '2-digit',
hour: 'numeric',
day: '2-digit',
month: 'short',
year: 'numeric'
})
</script>
<span class="text-sm" use:tooltip={{ label: getEmbeddedLabel(fullDate) }}>
{getDisplayTime(date)}
</span>

View File

@ -27,8 +27,8 @@
import { translate } from '@hcengineering/platform'
import { MessageViewer } from '@hcengineering/presentation'
import ActivityMessageTemplate from './ActivityMessageTemplate.svelte'
import ActivityMessageHeader from './ActivityMessageHeader.svelte'
import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte'
import ActivityMessageHeader from '../activity-message/ActivityMessageHeader.svelte'
export let value: ActivityInfoMessage
export let showNotify: boolean = false

View File

@ -0,0 +1,26 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ActivityInfoMessage, ActivityMessagePreviewType } from '@hcengineering/activity'
import BaseMessagePreview from '../activity-message/BaseMessagePreview.svelte'
export let value: ActivityInfoMessage
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
</script>
<BaseMessagePreview intlLabel={value.message} message={value} {type} {readonly} />

View File

@ -13,106 +13,58 @@
// limitations under the License.
-->
<script lang="ts">
import { Label } from '@hcengineering/ui'
import { ActivityInboxNotification, DocNotifyContext } from '@hcengineering/notification'
import activity, { ActivityMessage, DocUpdateMessage, Reaction } from '@hcengineering/activity'
import { Icon, Label } from '@hcengineering/ui'
import { DocNotifyContext } from '@hcengineering/notification'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { createQuery, getClient } from '@hcengineering/presentation'
import core, { Doc, Ref } from '@hcengineering/core'
import { getDocLinkTitle } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import { EmployeePresenter, personAccountByIdStore } from '@hcengineering/contact-resources'
import { PersonAccount } from '@hcengineering/contact'
import { Doc, Ref } from '@hcengineering/core'
import { classIcon, getDocLinkTitle } from '@hcengineering/view-resources'
import ActivityDocLink from '../ActivityDocLink.svelte'
import ReactionPresenter from '../reactions/ReactionPresenter.svelte'
import ActivityMessagePreview from './ActivityMessagePreview.svelte'
export let context: DocNotifyContext
export let notification: ActivityInboxNotification
const client = getClient()
const hierarchy = client.getHierarchy()
const parentQuery = createQuery()
const messageQuery = createQuery()
let parentMessage: ActivityMessage | undefined = undefined
let message: ActivityMessage | undefined = undefined
let title: string | undefined = undefined
let object: Doc | undefined = undefined
$: messageQuery.query(notification.attachedToClass, { _id: notification.attachedTo }, (res) => {
message = res[0]
})
$: parentQuery.query(activity.class.ActivityMessage, { _id: context.attachedTo as Ref<ActivityMessage> }, (res) => {
parentMessage = res[0]
})
$: parentMessage &&
getDocLinkTitle(client, parentMessage.attachedTo, parentMessage.attachedToClass).then((res) => {
title = res
})
$: parentMessage &&
client.findOne(parentMessage.attachedToClass, { _id: parentMessage.attachedTo }).then((res) => {
object = res
})
$: panelMixin = parentMessage
? hierarchy.classHierarchyMixin(parentMessage.attachedToClass, view.mixin.ObjectPanel)
: undefined
$: panelComponent = panelMixin?.component ?? view.component.EditDoc
$: isReaction =
message &&
hierarchy.isDerived(message._class, activity.class.DocUpdateMessage) &&
(message as DocUpdateMessage).objectClass === activity.class.Reaction
$: reaction = (isReaction ? (message as DocUpdateMessage).objectId : undefined) as Ref<Reaction> | undefined
$: personAccount =
message && $personAccountByIdStore.get((message?.createdBy ?? message.modifiedBy) as Ref<PersonAccount>)
$: object &&
getDocLinkTitle(client, object._id, object._class, object).then((res) => {
title = res
})
</script>
{#if object}
{#if reaction}
<div class="labels">
<div class="label overflow-label">
<Label label={activity.string.Message} />
<ActivityDocLink {title} preposition={activity.string.In} {object} {panelComponent} />
</div>
<div class="flex-baseline gap-2">
{#if personAccount?.person}
<EmployeePresenter value={personAccount.person} shouldShowAvatar={false} />
{:else}
<div class="strong">
<Label label={core.string.System} />
</div>
{/if}
<div class="lower">
<Label label={activity.string.Reacted} />
</div>
<ReactionPresenter _id={reaction} />
</div>
</div>
{:else}
<div class="label overflow-label">
<Label label={activity.string.Message} />
<ActivityDocLink {title} preposition={activity.string.In} {object} {panelComponent} />
</div>
{/if}
{#if parentMessage}
<span class="flex-presenter flex-gap-1 font-semi-bold">
<Label label={(parentMessage?.replies ?? 0) > 0 ? activity.string.Thread : activity.string.Message} />
{#if title}
<span class="lower">
<Label label={activity.string.In} />
</span>
{#if object}
{@const icon = classIcon(client, object._class)}
<span class="flex-presenter flex-gap-0-5">
{#if icon}
<Icon {icon} size="x-small" iconProps={{ value: object }} />
{/if}
{title}
</span>
{/if}
{/if}
</span>
<span class="font-normal">
<ActivityMessagePreview value={parentMessage} readonly type="content-only" />
</span>
{/if}
<style lang="scss">
.label {
width: 20rem;
max-width: 20rem;
}
.labels {
display: flex;
flex-direction: column;
justify-content: space-between;
}
</style>

View File

@ -0,0 +1,49 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { DisplayActivityMessage, ActivityMessagePreviewType } from '@hcengineering/activity'
import { getClient } from '@hcengineering/presentation'
import { Action, Component } from '@hcengineering/ui'
import { Class, Doc, Ref } from '@hcengineering/core'
import activity from '../../plugin'
export let value: DisplayActivityMessage
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
export let actions: Action[] = []
const client = getClient()
const hierarchy = client.getHierarchy()
$: previewMixin = hierarchy.classHierarchyMixin(
value._class as Ref<Class<Doc>>,
activity.mixin.ActivityMessagePreview
)
</script>
{#if previewMixin}
<Component
is={previewMixin.presenter}
props={{
value,
type,
readonly,
actions
}}
on:click
/>
{/if}

View File

@ -20,11 +20,10 @@
} from '@hcengineering/activity'
import { Person } from '@hcengineering/contact'
import { Avatar, EmployeePresenter, SystemAvatar } from '@hcengineering/contact-resources'
import core, { getDisplayTime } from '@hcengineering/core'
import core from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Action, Label, tooltip } from '@hcengineering/ui'
import { Action, Label } from '@hcengineering/ui'
import { getActions, restrictionStore } from '@hcengineering/view-resources'
import { getEmbeddedLabel } from '@hcengineering/platform'
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
import ActivityMessageExtensionComponent from './ActivityMessageExtension.svelte'
@ -33,6 +32,7 @@
import { isReactionMessage } from '../../activityMessagesUtils'
import Bookmark from '../icons/Bookmark.svelte'
import { savedMessagesStore } from '../../activity'
import MessageTimestamp from '../MessageTimestamp.svelte'
export let message: DisplayActivityMessage
export let parentMessage: DisplayActivityMessage | undefined = undefined
@ -109,14 +109,6 @@
let readonly: boolean = false
$: readonly = $restrictionStore.disableComments
$: fullDate = new Date(message.createdOn ?? message.modifiedOn).toLocaleString('default', {
minute: '2-digit',
hour: 'numeric',
day: '2-digit',
month: 'short',
year: 'numeric'
})
</script>
{#if !isHidden}
@ -173,19 +165,12 @@
{/if}
{#if !skipLabel && showDatePreposition}
<span class="text-sm lower">
<span class="text-sm lower mr-1">
<Label label={activity.string.At} />
</span>
{/if}
<span
class="text-sm"
use:tooltip={{
label: getEmbeddedLabel(fullDate)
}}
>
{getDisplayTime(message.createdOn ?? 0)}
</span>
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
</div>
<slot name="content" />
@ -249,7 +234,7 @@
}
&.selected {
background-color: var(--highlight-select);
background-color: var(--global-ui-highlight-BackgroundColor);
}
&.embedded {
@ -275,7 +260,7 @@
&.actionsOpened {
&.borderedHover {
border: 1px solid var(--highlight-hover);
border: 1px solid var(--global-ui-BackgroundColor);
}
&.filledHover {
@ -286,7 +271,7 @@
&.hoverable {
&:hover:not(.embedded) {
&.borderedHover {
border: 1px solid var(--highlight-hover);
border: 1px solid var(--global-ui-BackgroundColor);
}
&.filledHover {
@ -300,7 +285,7 @@
display: flex;
align-items: baseline;
font-size: 0.875rem;
color: var(--theme-halfcontent-color);
color: var(--global-secondary-TextColor);
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
@ -319,14 +304,14 @@
left: 0.25rem;
height: 0.5rem;
width: 0.5rem;
background-color: var(--theme-inbox-notify);
background-color: var(--global-higlight-Color);
border-radius: 50%;
}
.embeddedMarker {
width: 0.25rem;
border-radius: 0.5rem;
background: var(--secondary-button-default);
background: var(--global-ui-highlight-BackgroundColor);
}
.saveMarker {

View File

@ -0,0 +1,69 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { IntlString } from '@hcengineering/platform'
import activity, { ActivityMessage, ActivityMessagePreviewType } from '@hcengineering/activity'
import ActivityMessageActions from '../ActivityMessageActions.svelte'
import ReactionsPreview from '../reactions/ReactionsPreview.svelte'
import BasePreview from '../BasePreview.svelte'
import { Action } from '@hcengineering/ui'
export let text: string | undefined = undefined
export let intlLabel: IntlString | undefined = undefined
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
export let message: ActivityMessage
export let actions: Action[] = []
const client = getClient()
let previewElement: BasePreview
let isCompact = false
$: extensions = client.getModel().findAllSync(activity.class.ActivityMessageExtension, { ofMessage: message._class })
</script>
<BasePreview
bind:this={previewElement}
bind:isCompact
{text}
{intlLabel}
{readonly}
{type}
timestamp={message.createdOn ?? message.modifiedOn}
account={message.createdBy ?? message.modifiedBy}
on:click
>
<svelte:fragment slot="content">
<slot />
</svelte:fragment>
<svelte:fragment slot="right">
{#if type === 'full' && !isCompact}
<ReactionsPreview {message} {readonly} />
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<ActivityMessageActions
{message}
{extensions}
{actions}
on:open={previewElement.onActionsOpened}
on:close={previewElement.onActionsClosed}
/>
</svelte:fragment>
</BasePreview>

View File

@ -0,0 +1,28 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ActivityMessagePreviewType, ActivityReference } from '@hcengineering/activity'
import BaseMessagePreview from '../activity-message/BaseMessagePreview.svelte'
import { Action } from '@hcengineering/ui'
export let value: ActivityReference
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
export let actions: Action[] = []
</script>
<BaseMessagePreview text={value.message} message={value} {type} {readonly} {actions} on:click />

View File

@ -27,6 +27,7 @@
export let viewlet: DocUpdateMessageViewlet | undefined
export let attributeUpdates: DocAttributeUpdates
export let attributeModel: AttributeModel
export let preview = false
const client = getClient()
const hierarchy = client.getHierarchy()
@ -41,10 +42,10 @@
<Component is={presenter} props={{ value: attributeUpdates }} />
{:else}
{#if attributeUpdates.added.length}
<AddedAttributesPresenter {viewlet} {attributeModel} values={attributeUpdates.added} />
<AddedAttributesPresenter {viewlet} {attributeModel} values={attributeUpdates.added} {preview} />
{/if}
{#if attributeUpdates.removed.length}
<RemovedAttributesPresenter {viewlet} {attributeModel} values={attributeUpdates.removed} />
<RemovedAttributesPresenter {viewlet} {attributeModel} values={attributeUpdates.removed} {preview} />
{/if}
{#if attributeUpdates.set.length}
<SetAttributesPresenter
@ -52,6 +53,7 @@
{attributeModel}
values={attributeUpdates.set}
prevValue={attributeUpdates.prevValue}
{preview}
/>
{/if}
{/if}

View File

@ -24,6 +24,7 @@
export let objectName: IntlString | undefined
export let collectionName: IntlString | undefined
export let objectIcon: Asset | undefined
export let preview = false
const isOwn = message.objectId === message.attachedTo
@ -33,7 +34,7 @@
$: hasDifferentActions = message.previousMessages?.some(({ action }) => action !== message.action)
</script>
<div class="content">
<div class="content overflow-label" class:preview>
<span class="mr-1">
<Icon icon={viewlet?.icon ?? objectIcon ?? activity.icon.Activity} size="small" />
</span>
@ -52,35 +53,40 @@
{/if}
</span>
{#if hasDifferentActions}
{@const removeMessages = valueMessages.filter(({ action }) => action === 'remove')}
{@const createMessages = valueMessages.filter(({ action }) => action === 'create')}
<span class="overflow-label values" class:preview>
{#if hasDifferentActions}
{@const removeMessages = valueMessages.filter(({ action }) => action === 'remove')}
{@const createMessages = valueMessages.filter(({ action }) => action === 'create')}
{#each createMessages as valueMessage, index}
<DocUpdateMessageObjectValue
message={valueMessage}
{viewlet}
withIcon={index === 0}
hasSeparator={createMessages.length > 1 && index !== createMessages.length - 1}
/>
{/each}
{#each removeMessages as valueMessage, index}
<DocUpdateMessageObjectValue
message={valueMessage}
{viewlet}
withIcon={index === 0}
hasSeparator={removeMessages.length > 1 && index !== removeMessages.length - 1}
/>
{/each}
{:else}
{#each valueMessages as valueMessage, index}
<DocUpdateMessageObjectValue
message={valueMessage}
{viewlet}
hasSeparator={valueMessages.length > 1 && index !== valueMessages.length - 1}
/>
{/each}
{/if}
{#each createMessages as valueMessage, index}
<DocUpdateMessageObjectValue
message={valueMessage}
{viewlet}
withIcon={index === 0}
hasSeparator={createMessages.length > 1 && index !== createMessages.length - 1}
{preview}
/>
{/each}
{#each removeMessages as valueMessage, index}
<DocUpdateMessageObjectValue
message={valueMessage}
{viewlet}
withIcon={index === 0}
hasSeparator={removeMessages.length > 1 && index !== removeMessages.length - 1}
{preview}
/>
{/each}
{:else}
{#each valueMessages as valueMessage, index}
<DocUpdateMessageObjectValue
message={valueMessage}
{viewlet}
hasSeparator={valueMessages.length > 1 && index !== valueMessages.length - 1}
{preview}
/>
{/each}
{/if}
</span>
</div>
<style lang="scss">
@ -89,5 +95,20 @@
gap: 0.25rem;
align-items: center;
flex-wrap: wrap;
color: var(--global-primary-TextColor);
&.preview {
flex-wrap: nowrap;
}
}
.values {
display: flex;
align-items: center;
flex-wrap: wrap;
&.preview {
flex-wrap: nowrap;
}
}
</style>

View File

@ -16,7 +16,7 @@
import { buildRemovedDoc, checkIsObjectRemoved, DocNavLink, getDocLinkTitle } from '@hcengineering/view-resources'
import { Component, Icon, IconAdd, IconDelete } from '@hcengineering/ui'
import { createQuery, getClient } from '@hcengineering/presentation'
import view, { ObjectPanel, ObjectPresenter } from '@hcengineering/view'
import view from '@hcengineering/view'
import { Class, Doc, Ref } from '@hcengineering/core'
import { DisplayDocUpdateMessage, DocUpdateMessageViewlet } from '@hcengineering/activity'
@ -24,6 +24,7 @@
export let viewlet: DocUpdateMessageViewlet | undefined
export let withIcon: boolean = false
export let hasSeparator: boolean = false
export let preview = false
const client = getClient()
const hierarchy = client.getHierarchy()
@ -66,7 +67,13 @@
{/if}
{#if objectPresenter && !viewlet?.valueAttr}
<Component is={objectPresenter.presenter} props={{ value: object, accent: true, shouldShowAvatar: false }} />
<Component
is={objectPresenter.presenter}
props={{ value: object, accent: true, shouldShowAvatar: false, preview }}
/>
{#if hasSeparator}
<span class="ml-1" />
{/if}
{:else}
{#await getValue(object) then value}
<span class="valueLink">
@ -90,12 +97,12 @@
<style lang="scss">
.valueLink {
font-weight: 500;
color: var(--theme-link-color);
color: var(--global-primary-LinkColor);
}
.separator {
font-weight: 500;
color: var(--theme-link-color);
color: var(--global-primary-LinkColor);
margin-left: -0.25rem;
}
</style>

View File

@ -21,8 +21,8 @@
DocUpdateMessageViewlet
} from '@hcengineering/activity'
import { Person, PersonAccount } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources'
import core, { Account, AttachedDoc, Class, Collection, Doc, Ref } from '@hcengineering/core'
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { Account, AttachedDoc, Class, Collection, Doc, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Component, ShowMore, Action } from '@hcengineering/ui'
@ -58,7 +58,6 @@
const client = getClient()
const hierarchy = client.getHierarchy()
const userQuery = createQuery()
const objectQuery = createQuery()
const parentObjectQuery = createQuery()
@ -68,10 +67,9 @@
$: collectionAttribute = getCollectionAttribute(hierarchy, value.attachedToClass, value.updateCollection)
$: clazz = hierarchy.getClass(value.objectClass)
$: objectName = (collectionAttribute?.type as Collection<AttachedDoc>)?.itemLabel || clazz.label
$: objectName = (collectionAttribute?.type as Collection<AttachedDoc>)?.itemLabel ?? clazz.label
$: collectionName = collectionAttribute?.label
let user: PersonAccount | undefined = undefined
let person: Person | undefined = undefined
let viewlet: DocUpdateMessageViewlet | undefined
let attributeModel: AttributeModel | undefined = undefined
@ -98,11 +96,25 @@
parentMessage = res as DisplayActivityMessage
})
$: userQuery.query(core.class.Account, { _id: value.createdBy }, (res: Account[]) => {
user = res[0] as PersonAccount
})
$: person = getPerson(value.createdBy, $personAccountByIdStore, $personByIdStore)
$: person = user?.person != null ? $personByIdStore.get(user.person) : undefined
function getPerson (
_id: Ref<Account> | undefined,
accountById: Map<Ref<PersonAccount>, PersonAccount>,
personById: Map<Ref<Person>, Person>
): Person | undefined {
if (_id === undefined) {
return undefined
}
const personAccount = accountById.get(_id as Ref<PersonAccount>)
if (personAccount === undefined) {
return undefined
}
return personById.get(personAccount.person)
}
$: void loadObject(value.objectId, value.objectClass)
$: void loadParentObject(value, parentMessage)

View File

@ -0,0 +1,120 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import activity, {
ActivityMessagePreviewType,
DisplayDocUpdateMessage,
DocUpdateMessageViewlet
} from '@hcengineering/activity'
import { Action, Component } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import { IntlString } from '@hcengineering/platform'
import { AttachedDoc, Collection, Doc } from '@hcengineering/core'
import { AttributeModel } from '@hcengineering/view'
import { getAttributeModel, getCollectionAttribute } from '../../activityMessagesUtils'
import BaseMessagePreview from '../activity-message/BaseMessagePreview.svelte'
import DocUpdateMessageContent from './DocUpdateMessageContent.svelte'
import DocUpdateMessageAttributes from './DocUpdateMessageAttributes.svelte'
import { createEventDispatcher } from 'svelte'
export let value: DisplayDocUpdateMessage
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
export let actions: Action[] = []
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let viewlet: DocUpdateMessageViewlet | undefined
let objectName: IntlString | undefined = undefined
let collectionName: IntlString | undefined = undefined
let attributeModel: AttributeModel | undefined = undefined
let object: Doc | undefined
$: [viewlet] = client
.getModel()
.findAllSync(activity.class.DocUpdateMessageViewlet, { action: value.action, objectClass: value.objectClass })
$: collectionAttribute = getCollectionAttribute(hierarchy, value.attachedToClass, value.updateCollection)
$: clazz = hierarchy.getClass(value.objectClass)
$: objectName = (collectionAttribute?.type as Collection<AttachedDoc>)?.itemLabel ?? clazz.label
$: collectionName = collectionAttribute?.label
$: void getAttributeModel(client, value.attributeUpdates, value.objectClass).then((model) => {
attributeModel = model
})
function onClick (event: MouseEvent): void {
event.stopPropagation()
event.preventDefault()
dispatch('click')
}
</script>
<BaseMessagePreview message={value} {type} {readonly} {actions} on:click>
<span class="textContent overflow-label" class:contentOnly={type === 'content-only'}>
{#if viewlet?.component && object}
<div class="customContent">
{#each value?.previousMessages ?? [] as msg}
<Component
is={viewlet.component}
props={{ message: msg, _id: msg.objectId, _class: msg.objectClass, preview: true, onClick }}
/>
{/each}
<Component
is={viewlet.component}
props={{
message: value,
_id: value.objectId,
_class: value.objectClass,
preview: true,
value: object,
onClick
}}
/>
</div>
{:else if value.action === 'create' || value.action === 'remove'}
<DocUpdateMessageContent
message={value}
{viewlet}
{objectName}
{collectionName}
objectIcon={collectionAttribute?.icon ?? clazz.icon}
preview
/>
{:else if value.attributeUpdates && attributeModel}
<DocUpdateMessageAttributes attributeUpdates={value.attributeUpdates} {attributeModel} {viewlet} preview />
{/if}
</span>
</BaseMessagePreview>
<style lang="scss">
.textContent {
display: inline;
overflow: hidden;
max-height: 1.25rem;
color: var(--global-primary-TextColor);
margin-left: var(--spacing-0_5);
&.contentOnly {
margin-left: 0;
}
}
</style>

View File

@ -15,15 +15,17 @@
<script lang="ts">
import { Label } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import ChangeAttributesTemplate from './ChangeAttributesTemplate.svelte'
import activity, { DocAttributeUpdates, DocUpdateMessageViewlet } from '@hcengineering/activity'
import ChangeAttributesTemplate from './ChangeAttributesTemplate.svelte'
export let viewlet: DocUpdateMessageViewlet | undefined
export let attributeModel: AttributeModel
export let values: DocAttributeUpdates['added']
export let preview = false
</script>
<ChangeAttributesTemplate {viewlet} {attributeModel} {values}>
<ChangeAttributesTemplate {viewlet} {attributeModel} {values} {preview}>
<svelte:fragment slot="text">
<Label label={activity.string.New} />
<span class="lower"><Label label={attributeModel.label} />:</span>

View File

@ -26,12 +26,13 @@
export let viewlet: DocUpdateMessageViewlet | undefined
export let attributeModel: AttributeModel
export let values: Values
export let preview = false
const client = getClient()
let attributeValues: Values | Doc[] = []
$: getAttributeValues(client, values, attributeModel._class).then((result) => {
$: void getAttributeValues(client, values, attributeModel._class).then((result) => {
attributeValues = result
})
@ -40,7 +41,7 @@
$: space = typeof attributeValues[0] === 'object' ? attributeValues[0]?.space : undefined
</script>
<div class="content">
<div class="content overflow-label" class:preview>
<span class="mr-1">
{#if attrViewletConfig?.iconPresenter}
<Component is={attrViewletConfig?.iconPresenter} props={{ value: attributeValues[0], space, size: 'small' }} />
@ -52,7 +53,7 @@
<slot name="text" />
{#each attributeValues as value}
<span class="strong">
<span class="strong overflow-label">
{#if value !== null && typeof value === 'object'}
<ObjectPresenter {value} shouldShowAvatar={false} accent />
{:else}
@ -68,5 +69,10 @@
align-items: center;
flex-wrap: wrap;
gap: 0.25rem;
color: var(--global-primary-TextColor);
&.preview {
flex-wrap: nowrap;
}
}
</style>

View File

@ -22,9 +22,10 @@
export let viewlet: DocUpdateMessageViewlet | undefined
export let attributeModel: AttributeModel
export let values: DocAttributeUpdates['removed']
export let preview = false
</script>
<ChangeAttributesTemplate {viewlet} {attributeModel} {values}>
<ChangeAttributesTemplate {viewlet} {attributeModel} {values} {preview}>
<svelte:fragment slot="text">
<Label label={activity.string.Removed} />
<span class="lower"> <Label label={attributeModel.label} />:</span>

View File

@ -24,6 +24,7 @@
export let attributeModel: AttributeModel
export let values: DocAttributeUpdates['set']
export let prevValue: any
export let preview = false
$: attrViewletConfig = viewlet?.config?.[attributeModel.key]
$: attributeIcon = attrViewletConfig?.icon ?? attributeModel.icon ?? IconEdit
@ -46,7 +47,7 @@
</script>
{#if isUnset}
<div class="unset">
<div class="unset overflow-label">
<span class="mr-1"><Icon icon={attributeIcon} size="small" /></span>
<Label label={activity.string.Unset} />
<span class="lower"><Label label={attributeModel.label} /></span>
@ -62,7 +63,7 @@
<svelte:component this={attributeModel.presenter} value={values[0]} {prevValue} showOnlyDiff />
{/if}
{:else}
<ChangeAttributesTemplate {viewlet} {attributeModel} {values}>
<ChangeAttributesTemplate {viewlet} {attributeModel} {values} {preview}>
<svelte:fragment slot="text">
<Label label={attributeModel.label} />
<span class="lower"><Label label={activity.string.Set} /></span>
@ -79,7 +80,7 @@
}
.showMore {
color: var(--theme-link-color);
color: var(--global-primary-LinkColor);
cursor: pointer;
display: flex;
align-items: center;
@ -93,28 +94,28 @@
&.left {
border-top: 0.25rem solid transparent;
border-bottom: 0.25rem solid transparent;
border-left: 0.25rem solid var(--theme-link-color);
border-left: 0.25rem solid var(--global-primary-LinkColor);
border-right: none;
}
&.down {
border-left: 0.25rem solid transparent;
border-right: 0.25rem solid transparent;
border-top: 0.25rem solid var(--theme-link-color);
border-top: 0.25rem solid var(--global-primary-LinkColor);
border-bottom: none;
}
}
&:hover {
color: var(--theme-toggle-on-bg-hover);
color: var(--global-focus-BorderColor);
.triangle {
&.left {
border-left-color: var(--theme-toggle-on-bg-hover);
border-left-color: var(--global-focus-BorderColor);
}
&.down {
border-top-color: var(--theme-toggle-on-bg-hover);
border-top-color: var(--global-focus-BorderColor);
}
}
}

View File

@ -1,77 +0,0 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ActivityInboxNotification } from '@hcengineering/notification'
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import { createQuery } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import { Action, getLocation, navigate } from '@hcengineering/ui'
import ActivityMessagePresenter from '../activity-message/ActivityMessagePresenter.svelte'
export let message: DisplayActivityMessage
export let notification: ActivityInboxNotification
export let embedded = false
export let showNotify = true
export let withActions = true
export let actions: Action[] = []
export let excludedActions: string[] = []
export let onClick: (() => void) | undefined = undefined
const parentQuery = createQuery()
let parentMessage: ActivityMessage | undefined = undefined
$: embedded &&
parentQuery.query(activity.class.ActivityMessage, { _id: message.attachedTo as Ref<ActivityMessage> }, (res) => {
parentMessage = res[0]
})
function handleReply (): void {
const loc = getLocation()
loc.path[3] = notification.docNotifyContext
loc.path[4] = parentMessage?._id ?? message._id
loc.query = { message: parentMessage?._id ?? message._id }
navigate(loc)
}
</script>
{#if embedded && parentMessage}
<ActivityMessagePresenter
value={parentMessage}
skipLabel
embedded
{showNotify}
{withActions}
{actions}
{excludedActions}
hoverable={false}
onReply={handleReply}
{onClick}
/>
{:else if !embedded && message}
<ActivityMessagePresenter
value={message}
skipLabel
showEmbedded
{showNotify}
{withActions}
{actions}
{excludedActions}
hoverable={false}
onReply={handleReply}
{onClick}
/>
{/if}

View File

@ -94,7 +94,7 @@
.counter {
font-size: 0.75rem;
color: var(--theme-dark-color);
color: var(--global-secondary-TextColor);
margin-left: 0.25rem;
}
@ -104,15 +104,20 @@
justify-content: center;
width: 2.625rem;
height: 1.5rem;
background: var(--secondary-button-disabled);
background: var(--button-disabled-BackgroundColor);
border: none;
cursor: pointer;
&:hover {
border: 1px solid var(--theme-darker-color);
background: var(--global-ui-highlight-BackgroundColor);
}
&.withoutBackground {
background: transparent;
&:hover {
background: var(--global-ui-highlight-BackgroundColor);
}
}
}
</style>

View File

@ -0,0 +1,88 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
import { createQuery, getClient } from '@hcengineering/presentation'
import { EmojiPopup, showPopup } from '@hcengineering/ui'
import { SortingOrder } from '@hcengineering/core'
import { updateDocReactions } from '../../utils'
export let message: ActivityMessage | undefined
export let readonly = false
const maxPreviewReactions = 3
const client = getClient()
const reactionsQuery = createQuery()
let reactions: Reaction[] = []
let emojis: string[] = []
$: hasReactions = message?.reactions && message.reactions > 0
$: if (message && hasReactions) {
reactionsQuery.query(
activity.class.Reaction,
{ attachedTo: message._id },
(res: Reaction[]) => {
reactions = res
const result: string[] = []
for (const reaction of res) {
if (!result.includes(reaction.emoji)) {
result.push(reaction.emoji)
}
}
emojis = result
},
{
sort: {
createdOn: SortingOrder.Descending
}
}
)
} else {
reactionsQuery.unsubscribe()
}
function handleClick (e: MouseEvent): void {
if (readonly) return
e.stopPropagation()
e.preventDefault()
showPopup(EmojiPopup, {}, e.target as HTMLElement, (emoji: string) => {
void updateDocReactions(client, reactions, message, emoji)
})
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="preview" on:click={handleClick}>
{#each emojis.slice(0, maxPreviewReactions) as emoji}
{emoji}
{/each}
</span>
<style lang="scss">
.preview {
cursor: pointer;
white-space: nowrap;
margin-left: 0.5rem;
}
</style>

View File

@ -18,11 +18,13 @@ import { type Resources } from '@hcengineering/platform'
import Activity from './components/Activity.svelte'
import ActivityMessagePresenter from './components/activity-message/ActivityMessagePresenter.svelte'
import DocUpdateMessagePresenter from './components/doc-update-message/DocUpdateMessagePresenter.svelte'
import ActivityInfoMessagePresenter from './components/activity-message/ActivityInfoMessagePresenter.svelte'
import ActivityInfoMessagePresenter from './components/activity-info-message/ActivityInfoMessagePresenter.svelte'
import ReactionPresenter from './components/reactions/ReactionPresenter.svelte'
import ReactionNotificationPresenter from './components/reactions/ReactionNotificationPresenter.svelte'
import ActivityMessageNotificationLabel from './components/activity-message/ActivityMessageNotificationLabel.svelte'
import ActivityReferencePresenter from './components/activity-reference/ActivityReferencePresenter.svelte'
import DocUpdateMessagePreview from './components/doc-update-message/DocUpdateMessagePreview.svelte'
import ActivityReferencePreview from './components/activity-reference/ActivityReferencePreview.svelte'
import ActivityInfoMessagePreview from './components/activity-info-message/ActivityInfoMessagePreview.svelte'
import {
getMessageFragment,
@ -50,6 +52,10 @@ export { default as AddReactionAction } from './components/reactions/AddReaction
export { default as ActivityMessageAction } from './components/ActivityMessageAction.svelte'
export { default as ActivityMessagesFilterPopup } from './components/FilterPopup.svelte'
export { default as ActivityReferencePresenter } from './components/activity-reference/ActivityReferencePresenter.svelte'
export { default as ActivityMessagePreview } from './components/activity-message/ActivityMessagePreview.svelte'
export { default as MessageTimestamp } from './components/MessageTimestamp.svelte'
export { default as BaseMessagePreview } from './components/activity-message/BaseMessagePreview.svelte'
export { default as BasePreview } from './components/BasePreview.svelte'
export default async (): Promise<Resources> => ({
component: {
@ -58,9 +64,11 @@ export default async (): Promise<Resources> => ({
DocUpdateMessagePresenter,
ReactionPresenter,
ActivityInfoMessagePresenter,
ReactionNotificationPresenter,
ActivityMessageNotificationLabel,
ActivityReferencePresenter
ActivityReferencePresenter,
DocUpdateMessagePreview,
ActivityReferencePreview,
ActivityInfoMessagePreview
},
filter: {
AttributesFilter: attributesFilter,

View File

@ -274,6 +274,10 @@ export interface ActivityAttributeUpdatesPresenter extends Class<Doc> {
presenter: AnyComponent
}
export interface ActivityMessagePreview extends Class<Doc> {
presenter: AnyComponent
}
/**
* @public
*/
@ -309,10 +313,13 @@ export interface SavedMessage extends Preference {
*/
export interface IgnoreActivity extends Class<Doc> {}
export type ActivityMessagePreviewType = 'full' | 'content-only'
export default plugin(activityId, {
mixin: {
ActivityDoc: '' as Ref<Mixin<ActivityDoc>>,
ActivityAttributeUpdatesPresenter: '' as Ref<Mixin<ActivityAttributeUpdatesPresenter>>,
ActivityMessagePreview: '' as Ref<Mixin<ActivityMessagePreview>>,
IgnoreActivity: '' as Ref<Mixin<IgnoreActivity>>
},
class: {
@ -364,7 +371,9 @@ export default plugin(activityId, {
Mentioned: '' as IntlString,
You: '' as IntlString,
Mentions: '' as IntlString,
MentionedYouIn: '' as IntlString
MentionedYouIn: '' as IntlString,
Messages: '' as IntlString,
Thread: '' as IntlString
},
component: {
Activity: '' as AnyComponent,
@ -372,9 +381,11 @@ export default plugin(activityId, {
DocUpdateMessagePresenter: '' as AnyComponent,
ActivityInfoMessagePresenter: '' as AnyComponent,
ReactionPresenter: '' as AnyComponent,
ReactionNotificationPresenter: '' as AnyComponent,
ActivityMessageNotificationLabel: '' as AnyComponent,
ActivityReferencePresenter: '' as AnyComponent
ActivityReferencePresenter: '' as AnyComponent,
DocUpdateMessagePreview: '' as AnyComponent,
ActivityReferencePreview: '' as AnyComponent,
ActivityInfoMessagePreview: '' as AnyComponent
},
ids: {
AllFilter: '' as Ref<ActivityMessagesFilter>,

View File

@ -15,9 +15,9 @@
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
export let value: Attachment
export let value: Attachment | undefined
</script>
<div class="flex-row-center">
{#if value}
{value.name}
</div>
{/if}

View File

@ -20,9 +20,12 @@
import presentation, { PDFViewer, getFileUrl } from '@hcengineering/presentation'
import filesize from 'filesize'
import AttachmentName from './AttachmentName.svelte'
export let value: Attachment | undefined
export let removable: boolean = false
export let showPreview = false
export let preview = false
export let progress: boolean = false
@ -97,79 +100,83 @@
}
</script>
<div class="flex-row-center attachment-container">
{#if value}
<a
class="no-line"
style:flex-shrink={0}
href={getFileUrl(value.file, 'full', value.name)}
download={value.name}
on:click={clickHandler}
on:mousedown={middleClickHandler}
on:dragstart={dragStart}
>
{#if showPreview}
<div
class="flex-center icon"
class:svg={value.type === 'image/svg+xml'}
class:image={isImage(value.type)}
style={imgStyle}
>
{#if progress}
<div class="flex p-3">
<Loading />
</div>
{:else if !isImage(value.type)}{iconLabel(value.name)}{/if}
</div>
{:else}
<div class="flex-center icon">
{iconLabel(value.name)}
</div>
{/if}
</a>
<div class="flex-col info-container">
<div class="name">
<a
href={getFileUrl(value.file, 'full', value.name)}
download={value.name}
on:click={clickHandler}
on:mousedown={middleClickHandler}
>
{trimFilename(value.name)}
</a>
</div>
<div class="info-content flex-row-center">
{filesize(value.size, { spacer: '' })}
<span class="actions inline-flex clear-mins ml-1 gap-1">
<span></span>
{#if preview}
<AttachmentName {value} />
{:else}
<div class="flex-row-center attachment-container">
{#if value}
<a
class="no-line"
style:flex-shrink={0}
href={getFileUrl(value.file, 'full', value.name)}
download={value.name}
on:click={clickHandler}
on:mousedown={middleClickHandler}
on:dragstart={dragStart}
>
{#if showPreview}
<div
class="flex-center icon"
class:svg={value.type === 'image/svg+xml'}
class:image={isImage(value.type)}
style={imgStyle}
>
{#if progress}
<div class="flex p-3">
<Loading />
</div>
{:else if !isImage(value.type)}{iconLabel(value.name)}{/if}
</div>
{:else}
<div class="flex-center icon">
{iconLabel(value.name)}
</div>
{/if}
</a>
<div class="flex-col info-container">
<div class="name">
<a
class="no-line colorInherit"
href={getFileUrl(value.file, 'full', value.name)}
download={value.name}
bind:this={download}
on:click={clickHandler}
on:mousedown={middleClickHandler}
>
<Label label={presentation.string.Download} />
{trimFilename(value.name)}
</a>
{#if removable && value.readonly !== true}
</div>
<div class="info-content flex-row-center">
{filesize(value.size, { spacer: '' })}
<span class="actions inline-flex clear-mins ml-1 gap-1">
<span></span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class="remove-link"
on:click={(ev) => {
ev.stopPropagation()
ev.preventDefault()
dispatch('remove', value)
}}
<a
class="no-line colorInherit"
href={getFileUrl(value.file, 'full', value.name)}
download={value.name}
bind:this={download}
>
<Label label={presentation.string.Delete} />
</span>
{/if}
</span>
<Label label={presentation.string.Download} />
</a>
{#if removable && value.readonly !== true}
<span></span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class="remove-link"
on:click={(ev) => {
ev.stopPropagation()
ev.preventDefault()
dispatch('remove', value)
}}
>
<Label label={presentation.string.Delete} />
</span>
{/if}
</span>
</div>
</div>
</div>
{/if}
</div>
{/if}
</div>
{/if}
<style lang="scss">
.attachment-container {

View File

@ -0,0 +1,37 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Attachment } from '@hcengineering/attachment'
export let attachments: Attachment[] = []
</script>
<div class="tooltip">
{#each attachments as acc}
<div>
{acc.name}
</div>
{/each}
</div>
<style lang="scss">
.tooltip {
display: flex;
flex-direction: column;
gap: var(--spacing-1);
padding: var(--spacing-0_5);
}
</style>

View File

@ -21,11 +21,12 @@
import attachment from '../../plugin'
import AttachmentPresenter from '../AttachmentPresenter.svelte'
import RemovedAttachmentPresenter from '../RemovedAttachmentPresenter.svelte'
import AttachmentName from '../AttachmentName.svelte'
export let message: DocUpdateMessage
export let _id: Ref<Attachment>
export let value: Attachment | undefined = undefined
export let preview = false
const client = getClient()
@ -35,10 +36,8 @@
})
</script>
{#if message.action === 'remove'}
{#if value}
<RemovedAttachmentPresenter {value} />
{/if}
{#if preview || message.action === 'remove'}
<AttachmentName {value} />
{:else}
<AttachmentPresenter {value} />
{/if}

View File

@ -16,7 +16,7 @@
import type { Attachment } from '@hcengineering/attachment'
import core, { TxCUD, TxCreateDoc, TxProcessor } from '@hcengineering/core'
import AttachmentPresenter from '../AttachmentPresenter.svelte'
import RemovedAttachmentPresenter from '../RemovedAttachmentPresenter.svelte'
import AttachmentName from '../AttachmentName.svelte'
export let tx: TxCUD<Attachment>
export let value: any
@ -25,7 +25,7 @@
</script>
{#if tx._class === core.class.TxRemoveDoc}
<RemovedAttachmentPresenter value={doc} />
<AttachmentName value={doc} />
{:else}
<AttachmentPresenter value={doc} />
{/if}

View File

@ -41,6 +41,7 @@ import IconUploadDuo from './components/icons/UploadDuo.svelte'
import IconAttachment from './components/icons/Attachment.svelte'
import AttachmentPreview from './components/AttachmentPreview.svelte'
import AttachmentsUpdatedMessage from './components/activity/AttachmentsUpdatedMessage.svelte'
import AttachmentsTooltip from './components/AttachmentsTooltip.svelte'
import { deleteFile, uploadFile } from './utils'
export * from './types'
@ -63,7 +64,8 @@ export {
AccordionEditor,
IconUploadDuo,
IconAttachment,
AttachmentPreview
AttachmentPreview,
AttachmentsTooltip
}
export enum FileBrowserSortMode {

View File

@ -22,8 +22,8 @@
export let size: IconSize = 'small'
const inboxClient = InboxNotificationsClientImpl.getClient()
const store = inboxClient.docNotifyContextByDoc
$: subscribed = $store.get(object._id) !== undefined
const contextByDocStore = inboxClient.contextByDoc
$: subscribed = $contextByDocStore.get(object._id) !== undefined
</script>
{#if subscribed}

View File

@ -20,6 +20,7 @@
"@types/jest": "^29.5.5",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@types/html-to-text": "^8.1.1",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
@ -60,6 +61,7 @@
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/workbench-resources": "^0.6.1",
"fast-equals": "^2.0.3",
"svelte": "^4.2.12"
"svelte": "^4.2.12",
"html-to-text": "^9.0.3"
}
}

View File

@ -23,7 +23,7 @@
import Header from './Header.svelte'
import chunter from '../plugin'
import { getChannelIcon, getChannelName } from '../utils'
import { getObjectIcon, getChannelName } from '../utils'
import PinnedMessages from './PinnedMessages.svelte'
export let _id: Ref<Doc>
@ -65,7 +65,7 @@
<Header
bind:filters
{object}
icon={getChannelIcon(_class)}
icon={getObjectIcon(_class)}
iconProps={{ value: object }}
label={title}
intlLabel={chunter.string.Channel}

View File

@ -25,11 +25,11 @@
const objectQuery = createQuery()
const inboxClient = InboxNotificationsClientImpl.getClient()
const docNotifyContextByDocStore = inboxClient.docNotifyContextByDoc
const contextByDocStore = inboxClient.contextByDoc
let object: ChunterSpace | undefined = undefined
$: context = $docNotifyContextByDocStore.get(_id)
$: context = $contextByDocStore.get(_id)
$: objectQuery.query(_class, { _id }, (res) => {
object = res[0]

View File

@ -57,7 +57,7 @@
const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient()
const contextByDocStore = inboxClient.docNotifyContextByDoc
const contextByDocStore = inboxClient.contextByDoc
const filters = client.getModel().findAllSync(activity.class.ActivityMessagesFilter, {})
const messagesStore = provider.messagesStore
@ -610,7 +610,7 @@
<style lang="scss">
.msg {
margin: 0 1.5rem;
margin: 0;
min-height: 4.375rem;
height: auto;
display: flex;

View File

@ -46,10 +46,10 @@
let displayPersons: Person[] = []
$: docNotifyContextByDocStore = inboxClient?.docNotifyContextByDoc
$: contextByDocStore = inboxClient?.contextByDoc
$: notificationsByContextStore = inboxClient?.inboxNotificationsByContext
$: hasNew = hasNewReplies(object, $docNotifyContextByDocStore, $notificationsByContextStore)
$: hasNew = hasNewReplies(object, $contextByDocStore, $notificationsByContextStore)
$: updateQuery(persons, $personByIdStore)
function hasNewReplies (
@ -91,7 +91,7 @@
return
}
const context = get(inboxClient.docNotifyContextByDoc).get(object.attachedTo)
const context = get(inboxClient.contextByDoc).get(object.attachedTo)
if (context === undefined) {
return

View File

@ -62,7 +62,6 @@
dispatch('changeContent')
}}
on:keydown={(evt) => {
console.log(evt)
if (isTextMode) {
evt.preventDefault()
evt.stopImmediatePropagation()

View File

@ -0,0 +1,87 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createQuery } from '@hcengineering/presentation'
import { ChatMessage } from '@hcengineering/chunter'
import { BaseMessagePreview } from '@hcengineering/activity-resources'
import { Action, Icon, Label, tooltip } from '@hcengineering/ui'
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentsTooltip } from '@hcengineering/attachment-resources'
import { ActivityMessagePreviewType } from '@hcengineering/activity'
import { convert } from 'html-to-text'
export let value: ChatMessage
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
export let actions: Action[] = []
const attachmentsQuery = createQuery()
let attachments: Attachment[] = []
$: if (value.attachments !== undefined && value.attachments > 0) {
attachmentsQuery.query(
attachment.class.Attachment,
{
attachedTo: value._id
},
(res) => {
attachments = res
}
)
} else {
attachmentsQuery.unsubscribe()
}
$: text = value.message
? convert(value.message, {
preserveNewlines: false,
selectors: [{ selector: 'img', format: 'skip' }]
})
: undefined
</script>
<BaseMessagePreview text={text ? value.message : undefined} message={value} {type} {readonly} {actions} on:click>
{#if value.attachments && type === 'full' && text}
<div class="attachments" use:tooltip={{ component: AttachmentsTooltip, props: { attachments } }}>
{value.attachments}
<Icon icon={attachment.icon.Attachment} size="small" />
</div>
{:else if attachments.length > 0 && !text}
<span class="font-normal">
<Label label={attachment.string.Attachments} />:
<span class="ml-1">
{attachments.map(({ name }) => name).join(', ')}
</span>
</span>
{/if}
</BaseMessagePreview>
<style lang="scss">
.attachments {
margin-left: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--global-secondary-TextColor);
&:hover {
cursor: pointer;
color: var(--global-primary-TextColor);
}
}
</style>

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Doc, Ref } from '@hcengineering/core'
import { Doc, IdMap, Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import {
Component,
@ -39,7 +39,7 @@
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
const notificationsClient = InboxNotificationsClientImpl.getClient()
const contextsStore = notificationsClient.docNotifyContexts
const contextByIdStore = notificationsClient.contextById
const objectQuery = createQuery()
const navigatorModel: NavigatorModel = {
@ -58,7 +58,7 @@
syncLocation(loc)
})
$: updateSelectedContext($contextsStore, selectedContextId)
$: updateSelectedContext($contextByIdStore, selectedContextId)
$: selectedContext &&
objectQuery.query(
@ -83,11 +83,11 @@
}
}
function updateSelectedContext (contexts: DocNotifyContext[], _id?: Ref<DocNotifyContext>) {
function updateSelectedContext (contexts: IdMap<DocNotifyContext>, _id?: Ref<DocNotifyContext>) {
if (selectedContextId === undefined) {
selectedContext = undefined
} else {
selectedContext = contexts.find(({ _id }) => _id === selectedContextId)
selectedContext = contexts.get(selectedContextId)
}
}

View File

@ -25,14 +25,14 @@
export let _id: Ref<Doc>
const notificationsClient = InboxNotificationsClientImpl.getClient()
const contextsStore = notificationsClient.docNotifyContexts
const contextByIdStore = notificationsClient.contextById
const objectQuery = createQuery()
let threadId: Ref<ActivityMessage> | undefined = undefined
let context: DocNotifyContext | undefined = undefined
let object: Doc | undefined = undefined
$: context = $contextsStore.find((context) => context._id === _id)
$: context = $contextByIdStore.get(_id as Ref<DocNotifyContext>)
$: threadId = context ? undefined : (_id as Ref<ActivityMessage>)
$: context &&

View File

@ -16,15 +16,16 @@
import { Doc, Ref } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
import ui, { Action, IconSize, ModernButton } from '@hcengineering/ui'
import ui, { Action, AnySvelteComponent, IconSize, ModernButton } from '@hcengineering/ui'
import { getDocTitle } from '@hcengineering/view-resources'
import contact from '@hcengineering/contact'
import { translate } from '@hcengineering/platform'
import { getResource, translate } from '@hcengineering/platform'
import view from '@hcengineering/view'
import ChatNavItem from './ChatNavItem.svelte'
import chunter from '../../../plugin'
import { ChatNavItemModel } from '../types'
import { getChannelIcon, getChannelName } from '../../../utils'
import { getObjectIcon, getChannelName } from '../../../utils'
import ChatSectionHeader from './ChatSectionHeader.svelte'
export let header: string
@ -58,7 +59,7 @@
for (const object of objects) {
const { _class } = object
const icon = getChannelIcon(_class)
const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon)
const titleIntl = client.getHierarchy().getClass(_class).label
const isPerson = hierarchy.isDerived(_class, contact.class.Person)
@ -67,12 +68,18 @@
const iconSize: IconSize = isDirect || isPerson ? 'x-small' : 'small'
let icon: AnySvelteComponent | undefined = undefined
if (iconMixin?.component) {
icon = await getResource(iconMixin.component)
}
items.push({
id: object._id,
object,
title: (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})),
description: isDocChat && !isPerson ? await getDocTitle(client, object._id, object._class, object) : undefined,
icon,
icon: icon ?? getObjectIcon(_class),
iconSize,
withIconBackground: !isDirect && !isPerson,
isSecondary: isDocChat && !isPerson

View File

@ -33,7 +33,7 @@
export let currentSpecial: SpecialNavModel | undefined
const notificationClient = InboxNotificationsClientImpl.getClient()
const contextsStore = notificationClient.docNotifyContexts
const contextsStore = notificationClient.contexts
const globalActions = [
{
@ -54,14 +54,14 @@
const searchValue: string = ''
async function isSpecialVisible (special: SpecialNavModel, docNotifyContexts: DocNotifyContext[]): Promise<boolean> {
async function isSpecialVisible (special: SpecialNavModel, contexts: DocNotifyContext[]): Promise<boolean> {
if (special.visibleIf === undefined) {
return true
}
const getIsVisible = await getResource(special.visibleIf)
return await getIsVisible(docNotifyContexts as any)
return await getIsVisible(contexts as any)
}
function addButtonClicked (ev: MouseEvent): void {

View File

@ -43,7 +43,7 @@
import { get } from 'svelte/store'
import notification from '@hcengineering/notification'
import { getChannelIcon, joinChannel, leaveChannel } from '../../../utils'
import { getObjectIcon, joinChannel, leaveChannel } from '../../../utils'
import chunter from './../../../plugin'
export let _class: Ref<Class<Channel>> = chunter.class.Channel
@ -115,7 +115,7 @@
async function view (channel: Channel): Promise<void> {
const loc = getCurrentResolvedLocation()
const context = get(notificationClient.docNotifyContextByDoc).get(channel._id)
const context = get(notificationClient.contextByDoc).get(channel._id)
let contextId = context?._id
@ -185,7 +185,7 @@
<Scroller padding={'2.5rem'}>
<div class="spaces-container">
{#each channels as channel (channel._id)}
{@const icon = getChannelIcon(channel._class)}
{@const icon = getObjectIcon(channel._class)}
{@const joined = channel.members.includes(me)}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="item flex-between" tabindex="0">

View File

@ -13,62 +13,86 @@
// limitations under the License.
-->
<script lang="ts">
import { Label } from '@hcengineering/ui'
import { ActivityInboxNotification, DocNotifyContext } from '@hcengineering/notification'
import { ActivityDocLink, ActivityMessageNotificationLabel } from '@hcengineering/activity-resources'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Doc, Ref } from '@hcengineering/core'
import { Icon, Label } from '@hcengineering/ui'
import { DocNotifyContext } from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
import { Class, Doc, Ref } from '@hcengineering/core'
import { getDocLinkTitle } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import { ChatMessage, ThreadMessage } from '@hcengineering/chunter'
import chunter from '../../plugin'
import ChatMessagePreview from '../chat-message/ChatMessagePreview.svelte'
import ThreadMessagePreview from '../threads/ThreadMessagePreview.svelte'
import { getObjectIcon } from '../../utils'
export let context: DocNotifyContext
export let notification: ActivityInboxNotification
const client = getClient()
const hierarchy = client.getHierarchy()
const parentQuery = createQuery()
let parentMessage: ActivityMessage | undefined = undefined
let title: string | undefined = undefined
let parentMessage: ChatMessage | undefined = undefined
let object: Doc | undefined = undefined
$: isThread = hierarchy.isDerived(notification.attachedToClass, chunter.class.ThreadMessage)
$: isThread &&
parentQuery.query(activity.class.ActivityMessage, { _id: context.attachedTo as Ref<ActivityMessage> }, (res) => {
parentMessage = res[0]
$: isThread = hierarchy.isDerived(context.attachedToClass, chunter.class.ThreadMessage)
$: void client
.findOne(context.attachedToClass as Ref<Class<ChatMessage>>, { _id: context.attachedTo as Ref<ChatMessage> })
.then((res) => {
parentMessage = res
})
$: parentMessage &&
getDocLinkTitle(client, parentMessage.attachedTo, parentMessage.attachedToClass).then((res) => {
$: loadObject(parentMessage, isThread)
$: object &&
getDocLinkTitle(client, object._id, object._class, object).then((res) => {
title = res
})
$: parentMessage &&
client.findOne(parentMessage.attachedToClass, { _id: parentMessage.attachedTo }).then((res) => {
function loadObject (parentMessage: ChatMessage | undefined, isThread: boolean): void {
if (parentMessage === undefined) {
object = undefined
return
}
const _class = isThread ? (parentMessage as ThreadMessage).objectClass : parentMessage.attachedToClass
const _id = isThread ? (parentMessage as ThreadMessage).objectId : parentMessage.attachedTo
void client.findOne(_class, { _id }).then((res) => {
object = res
})
}
$: panelMixin = parentMessage
? hierarchy.classHierarchyMixin(parentMessage.attachedToClass, view.mixin.ObjectPanel)
: undefined
$: panelComponent = panelMixin?.component ?? view.component.EditDoc
function toThread (message: ChatMessage): ThreadMessage {
return message as ThreadMessage
}
$: icon = object ? getObjectIcon(object._class) : undefined
</script>
{#if isThread && object}
<div class="label overflow-label">
<Label label={chunter.string.Thread} />
<ActivityDocLink {title} preposition={activity.string.In} {object} {panelComponent} />
</div>
{:else if !isThread}
<ActivityMessageNotificationLabel {context} {notification} />
{#if parentMessage}
<span class="flex-presenter flex-gap-1 font-semi-bold">
{#if isThread || (parentMessage.replies ?? 0) > 0}
<Label label={chunter.string.Thread} />
{:else}
<Label label={chunter.string.Message} />
{/if}
{#if title}
<span class="lower">
<Label label={chunter.string.In} />
</span>
<span class="flex-presenter flex-gap-0-5">
{#if icon}
<Icon {icon} size="x-small" iconProps={{ value: object }} />
{/if}
{title}
</span>
{/if}
</span>
<span class="font-normal">
{#if isThread}
<ThreadMessagePreview value={toThread(parentMessage)} readonly type="content-only" />
{:else}
<ChatMessagePreview value={parentMessage} readonly type="content-only" />
{/if}
</span>
{/if}
<style lang="scss">
.label {
width: 20rem;
max-width: 20rem;
}
</style>

View File

@ -14,28 +14,9 @@
-->
<script lang="ts">
import { ThreadMessage } from '@hcengineering/chunter'
import ThreadMessagePresenter from '../threads/ThreadMessagePresenter.svelte'
import { Action } from '@hcengineering/ui'
import { ActivityInboxNotification } from '@hcengineering/notification'
import ThreadMessagePreview from '../threads/ThreadMessagePreview.svelte'
export let message: ThreadMessage
export let notification: ActivityInboxNotification
export let embedded = false
export let showNotify = true
export let withActions = true
export let actions: Action[] = []
export let excludedActions: string[] = []
export let onClick: (() => void) | undefined = undefined
</script>
<ThreadMessagePresenter
value={message}
{embedded}
showEmbedded={!embedded}
{withActions}
{actions}
{excludedActions}
hoverable={false}
{showNotify}
{onClick}
/>
<ThreadMessagePreview value={message} on:click />

View File

@ -0,0 +1,29 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ThreadMessage } from '@hcengineering/chunter'
import { ActivityMessagePreviewType } from '@hcengineering/activity'
import ChatMessagePreview from '../chat-message/ChatMessagePreview.svelte'
import { Action } from '@hcengineering/ui'
export let value: ThreadMessage
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
export let actions: Action[] = []
</script>
<ChatMessagePreview {value} {readonly} {type} {actions} on:click />

View File

@ -23,7 +23,7 @@
import chunter from '../../plugin'
import ThreadParentMessage from './ThreadParentPresenter.svelte'
import { getChannelIcon, getChannelName } from '../../utils'
import { getObjectIcon, getChannelName } from '../../utils'
import ChannelScrollView from '../ChannelScrollView.svelte'
import { ChannelDataProvider } from '../../channelDataProvider'
@ -78,7 +78,7 @@
return [
{
icon: getChannelIcon(message.attachedToClass),
icon: getObjectIcon(message.attachedToClass),
iconProps: { value: channel },
iconWidth: isPersonAvatar ? 'auto' : undefined,
withoutIconBackground: isPersonAvatar,

View File

@ -53,6 +53,8 @@ import ChatMessageNotificationLabel from './components/notification/ChatMessageN
import ChatAside from './components/chat/ChatAside.svelte'
import Replies from './components/Replies.svelte'
import ReplyToThreadAction from './components/ReplyToThreadAction.svelte'
import ThreadMessagePreview from './components/threads/ThreadMessagePreview.svelte'
import ChatMessagePreview from './components/chat-message/ChatMessagePreview.svelte'
import {
ChannelTitleProvider,
@ -180,7 +182,7 @@ export async function replyToThread (message: ActivityMessage): Promise<void> {
const inboxClient = InboxNotificationsClientImpl.getClient()
let contextId: Ref<DocNotifyContext> | undefined = get(inboxClient.docNotifyContextByDoc).get(message.attachedTo)?._id
let contextId: Ref<DocNotifyContext> | undefined = get(inboxClient.contextByDoc).get(message.attachedTo)?._id
if (contextId === undefined) {
contextId = await client.createDoc(notification.class.DocNotifyContext, message.space, {
@ -237,7 +239,9 @@ export default async (): Promise<Resources> => ({
ThreadNotificationPresenter,
ChatAside,
Replies,
ReplyToThreadAction
ReplyToThreadAction,
ThreadMessagePreview,
ChatMessagePreview
},
function: {
GetDmName: getDmName,

View File

@ -214,7 +214,7 @@ export async function openMessageFromSpecial (message?: ActivityMessage): Promis
loc.path[4] = threadMessage.attachedTo
} else {
const context = get(inboxClient.docNotifyContextByDoc).get(message.attachedTo)
const context = get(inboxClient.contextByDoc).get(message.attachedTo)
if (context === undefined) {
return
@ -252,9 +252,9 @@ export async function getMessageLink (message: ActivityMessage): Promise<string>
if (message._class === chunter.class.ThreadMessage) {
const threadMessage = message as ThreadMessage
threadParent = `/${threadMessage.attachedTo}`
context = get(inboxClient.docNotifyContextByDoc).get(threadMessage.objectId)
context = get(inboxClient.contextByDoc).get(threadMessage.objectId)
} else {
context = get(inboxClient.docNotifyContextByDoc).get(message.attachedTo)
context = get(inboxClient.contextByDoc).get(message.attachedTo)
}
if (context === undefined) {
@ -279,7 +279,7 @@ export async function getTitle (doc: Doc): Promise<string> {
export async function chunterSpaceLinkFragmentProvider (doc: ChunterSpace): Promise<Location> {
const inboxClient = InboxNotificationsClientImpl.getClient()
const context = get(inboxClient.docNotifyContextByDoc).get(doc._id)
const context = get(inboxClient.contextByDoc).get(doc._id)
const loc = getCurrentResolvedLocation()
if (context === undefined) {
@ -295,7 +295,7 @@ export async function chunterSpaceLinkFragmentProvider (doc: ChunterSpace): Prom
return loc
}
export function getChannelIcon (_class: Ref<Class<Doc>>): Asset | AnySvelteComponent | undefined {
export function getObjectIcon (_class: Ref<Class<Doc>>): Asset | AnySvelteComponent | undefined {
const client = getClient()
const hierarchy = client.getHierarchy()
@ -412,7 +412,7 @@ export async function getThreadLink (doc: ThreadMessage): Promise<Location> {
const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient()
let contextId: Ref<DocNotifyContext> | undefined = get(inboxClient.docNotifyContextByDoc).get(doc.objectId)?._id
let contextId: Ref<DocNotifyContext> | undefined = get(inboxClient.contextByDoc).get(doc.objectId)?._id
if (contextId === undefined) {
contextId = await client.createDoc(notification.class.DocNotifyContext, doc.space, {

View File

@ -141,7 +141,9 @@ export default plugin(chunterId, {
ThreadMessagePresenter: '' as AnyComponent,
ChatAside: '' as AnyComponent,
Replies: '' as AnyComponent,
ReplyToThreadAction: '' as AnyComponent
ReplyToThreadAction: '' as AnyComponent,
ChatMessagePreview: '' as AnyComponent,
ThreadMessagePreview: '' as AnyComponent
},
class: {
Message: '' as Ref<Class<Message>>,

View File

@ -50,12 +50,12 @@
export let focusIndex = -1
export let restricted: Ref<ChannelProvider>[] = []
let notifyContextByDocStore: Writable<Map<Ref<Doc>, DocNotifyContext>> = writable(new Map())
let contextByDocStore: Writable<Map<Ref<Doc>, DocNotifyContext>> = writable(new Map())
let inboxNotificationsByContextStore: Readable<Map<Ref<DocNotifyContext>, InboxNotification[]>> = readable(new Map())
getResource(notification.function.GetInboxNotificationsClient).then((res) => {
const inboxClient = res()
notifyContextByDocStore = inboxClient.docNotifyContextByDoc
contextByDocStore = inboxClient.contextByDoc
inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
})
@ -149,7 +149,7 @@
updateMenu(displayItems, channelProviders)
}
$: if (value) update(value, $notifyContextByDocStore, $inboxNotificationsByContextStore, $channelProviders)
$: if (value) update(value, $contextByDocStore, $inboxNotificationsByContextStore, $channelProviders)
let displayItems: Item[] = []
let actions: Action[] = []
@ -171,7 +171,7 @@
const provider = getProvider(
{ provider: pr._id, value: '' },
toIdMap(providers),
$notifyContextByDocStore,
$contextByDocStore,
$inboxNotificationsByContextStore
)
if (provider !== undefined) {

View File

@ -32,12 +32,12 @@
export let reverse: boolean = false
export let integrations: Set<Ref<Doc>> = new Set<Ref<Doc>>()
let notifyContextByDocStore: Writable<Map<Ref<Doc>, DocNotifyContext>> = writable(new Map())
let contextByDocStore: Writable<Map<Ref<Doc>, DocNotifyContext>> = writable(new Map())
let inboxNotificationsByContextStore: Readable<Map<Ref<DocNotifyContext>, InboxNotification[]>> = readable(new Map())
getResource(notification.function.GetInboxNotificationsClient).then((res) => {
const inboxClient = res()
notifyContextByDocStore = inboxClient.docNotifyContextByDoc
contextByDocStore = inboxClient.contextByDoc
inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
})
@ -121,7 +121,7 @@
displayItems = result
}
$: if (value) update(value, $notifyContextByDocStore, $inboxNotificationsByContextStore, $channelProviders)
$: if (value) update(value, $contextByDocStore, $inboxNotificationsByContextStore, $channelProviders)
let displayItems: Item[] = []
let divHTML: HTMLElement

View File

@ -55,6 +55,21 @@
border-radius: 0.5rem;
}
&.card {
width: 1.25rem;
height: 1.25rem;
.text {
font-weight: 500;
font-size: 0.625rem;
letter-spacing: -0.05em;
}
&.roundedRect {
border-radius: 0.25rem;
}
}
&.x-small {
width: 1.5rem;
height: 1.5rem;

View File

@ -109,7 +109,7 @@ export async function filterChannelHasNewMessagesResult (
filter,
onUpdate,
undefined,
get(inboxClient.docNotifyContextByDoc),
get(inboxClient.contextByDoc),
get(inboxClient.inboxNotificationsByContext)
)
return { $in: result }

View File

@ -19,6 +19,7 @@
"Change": "Change",
"AddedRemoved": "Added/removed",
"YouAddedCollaborators": "You have been added to collaborators",
"YouRemovedCollaborators": "You have been removed from collaborators",
"YouHaveJoinedTheConversation": "You have joined the conversation",
"ChangeCollaborators": "changed collaborators",
"Activity": "Activity",
@ -34,14 +35,13 @@
"Edited": "edited",
"Pinned": "Pinned",
"Message": "Message",
"FlatList": "Flat list",
"GroupedList": "Grouped list",
"ArchiveAll": "Archive all",
"MarkReadAll": "Mark all as read",
"MarkUnreadAll": "Mark all as unread",
"ArchiveAllConfirmationTitle": "Archive all notifications?",
"ArchiveAllConfirmationMessage": "Are you sure you want to archive all notifications? This operation cannot be undone.",
"StarDocument": "Star document",
"UnstarDocument": "Unstar document"
"UnstarDocument": "Unstar document",
"Unsubscribe": "Unsubscribe"
}
}

View File

@ -33,8 +33,6 @@
"RemovedCollaborators": "Colaboradores Eliminados",
"Edited": "Editado",
"Pinned": "Fijado",
"Message": "Mensaje",
"FlatList": "Lista Plana",
"GroupedList": "Lista Agrupada"
"Message": "Mensaje"
}
}

View File

@ -33,8 +33,6 @@
"RemovedCollaborators": "Colaboradores removidos",
"Edited": "Editado",
"Pinned": "Fixado",
"Message": "Mensagem",
"FlatList": "Lista Plana",
"GroupedList": "Lista Agrupada"
"Message": "Mensagem"
}
}

View File

@ -19,6 +19,7 @@
"Change": "Изменено",
"AddedRemoved": "Добавлено/удалено",
"YouAddedCollaborators": "Вы были добавлены как участник",
"YouRemovedCollaborators": "Вы были удалены из участников",
"YouHaveJoinedTheConversation": "Вы присоединились к диалогу",
"ChangeCollaborators": "изменил(а) участники",
"Activity": "Активность",
@ -34,14 +35,13 @@
"Edited": "отредактировал(а)",
"Pinned": "Закреплено",
"Message": "Сообщение",
"FlatList": "Flat list",
"GroupedList": "Grouped list",
"ArchiveAll": "Архивировать все",
"MarkReadAll": "Oтметить все как прочитанное",
"MarkUnreadAll": "Отметить все как непрочитанные",
"ArchiveAllConfirmationTitle": "Архивировать все уведомления?",
"ArchiveAllConfirmationMessage": "Вы уверены, что хотите заархивировать все уведомления? Эту операцию невозможно отменить.",
"StarDocument": "Добавить в избранное",
"UnstarDocument": "Удалить из избранного"
"UnstarDocument": "Удалить из избранного",
"Unsubscribe": "Отписаться"
}
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { ActionIcon, Component, IconMoreH, Label, showPopup } from '@hcengineering/ui'
import { ButtonIcon, CheckBox, Component, IconMoreV, Label, showPopup, Spinner } from '@hcengineering/ui'
import notification, {
ActivityNotificationViewlet,
DisplayInboxNotification,
@ -21,37 +21,53 @@
} from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
import { getDocTitle, getDocIdentifier, Menu } from '@hcengineering/view-resources'
import chunter from '@hcengineering/chunter'
import { createEventDispatcher } from 'svelte'
import { WithLookup } from '@hcengineering/core'
import InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte'
import NotifyContextIcon from './NotifyContextIcon.svelte'
import NotifyMarker from './NotifyMarker.svelte'
import { deleteContextNotifications } from '../utils'
export let value: DocNotifyContext
export let visibleNotification: WithLookup<DisplayInboxNotification>
export let notifications: WithLookup<DisplayInboxNotification>[]
export let viewlets: ActivityNotificationViewlet[] = []
export let isCompact = true
export let unreadCount = 0
const maxNotifications = 3
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let isActionMenuOpened = false
let unreadCount = 0
$: unreadCount = notifications.filter(({ isViewed }) => !isViewed).length
let idTitle: string | undefined
let title: string | undefined
$: void getDocIdentifier(client, value.attachedTo, value.attachedToClass).then((res) => {
idTitle = res
})
$: void getDocTitle(client, value.attachedTo, value.attachedToClass).then((res) => {
title = res
})
$: presenterMixin = hierarchy.classHierarchyMixin(
value.attachedToClass,
notification.mixin.NotificationContextPresenter
)
function showMenu (ev: MouseEvent): void {
ev.stopPropagation()
ev.preventDefault()
showPopup(
Menu,
{
object: value,
baseMenuClass: notification.class.DocNotifyContext,
excludedActions: [
notification.action.PinDocNotifyContext,
notification.action.UnpinDocNotifyContext,
chunter.action.OpenChannel
]
mode: 'panel'
},
ev.target as HTMLElement,
handleActionMenuClosed
@ -59,12 +75,6 @@
handleActionMenuOpened()
}
$: presenterMixin = hierarchy.classHierarchyMixin(
value.attachedToClass,
notification.mixin.NotificationContextPresenter
)
let isActionMenuOpened = false
function handleActionMenuOpened (): void {
isActionMenuOpened = true
}
@ -73,61 +83,80 @@
isActionMenuOpened = false
}
$: getDocIdentifier(client, value.attachedTo, value.attachedToClass).then((res) => {
idTitle = res
})
let deletingPromise: Promise<any> | undefined = undefined
$: getDocTitle(client, value.attachedTo, value.attachedToClass).then((res) => {
title = res
})
async function checkContext (): Promise<void> {
await deletingPromise
deletingPromise = deleteContextNotifications(value)
await deletingPromise
deletingPromise = undefined
}
</script>
{#if visibleNotification}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="card"
class:compact={isCompact}
on:click={() => {
dispatch('click', { context: value, notification: visibleNotification })
}}
>
{#if isCompact}
<InboxNotificationPresenter value={visibleNotification} {viewlets} showNotify={false} withFlatActions />
<div class="notifyMarker compact">
<NotifyMarker count={unreadCount} />
</div>
{:else}
<div class="header">
<NotifyContextIcon {value} />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="card"
on:click={() => {
dispatch('click', { context: value })
}}
>
<div class="header">
<NotifyContextIcon {value} notifyCount={unreadCount} />
{#if presenterMixin?.labelPresenter}
<Component is={presenterMixin.labelPresenter} props={{ notification: visibleNotification, context: value }} />
<div class="labels">
{#if presenterMixin?.labelPresenter}
<Component is={presenterMixin.labelPresenter} props={{ context: value }} />
{:else}
{#if idTitle}
{idTitle}
{:else}
<div class="labels">
{#if idTitle}
{idTitle}
{:else}
<Label label={hierarchy.getClass(value.attachedToClass).label} />
{/if}
<div class="title overflow-label" {title}>
{title ?? hierarchy.getClass(value.attachedToClass).label}
</div>
</div>
<Label label={hierarchy.getClass(value.attachedToClass).label} />
{/if}
<span class="title overflow-label clear-mins" {title}>
{title ?? hierarchy.getClass(value.attachedToClass).label}
</span>
{/if}
</div>
<div class="actions clear-mins">
<div class="flex-center">
{#if deletingPromise !== undefined}
<Spinner size="small" />
{:else}
<CheckBox checked={false} kind="todo" size="medium" on:value={checkContext} />
{/if}
</div>
<div class="actions clear-mins flex flex-gap-2 items-center" class:opened={isActionMenuOpened}>
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
</div>
<div class="notifyMarker">
<NotifyMarker count={unreadCount} />
</div>
<div class="notification">
<InboxNotificationPresenter value={visibleNotification} {viewlets} embedded skipLabel />
</div>
{/if}
<ButtonIcon
icon={IconMoreV}
size="small"
kind="tertiary"
inheritColor
pressed={isActionMenuOpened}
on:click={showMenu}
/>
</div>
</div>
{/if}
<div class="content">
<div class="notifications">
{#each notifications.slice(0, maxNotifications).reverse() as notification}
<div class="notification">
<div class="embeddedMarker" />
<InboxNotificationPresenter
value={notification}
{viewlets}
on:click={(e) => {
e.preventDefault()
e.stopPropagation()
dispatch('click', { context: value, notification })
}}
/>
</div>
{/each}
</div>
</div>
</div>
<style lang="scss">
.card {
@ -135,65 +164,93 @@
position: relative;
flex-direction: column;
cursor: pointer;
border: 1px solid transparent;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
padding-right: 0;
margin: 0.5rem 0;
&.compact {
padding: 0;
margin: 0;
}
padding: var(--spacing-1_5) var(--spacing-1);
border-bottom: 1px solid var(--global-ui-BorderColor);
.header {
position: relative;
display: flex;
align-items: center;
gap: 1.25rem;
margin-left: 0.25rem;
}
gap: 0.75rem;
margin-left: var(--spacing-0_5);
.title {
font-weight: 500;
max-width: 20.5rem;
}
.actions {
position: absolute;
visibility: hidden;
top: 0.75rem;
right: 0.75rem;
color: var(--theme-halfcontent-color);
&.opened {
visibility: visible;
.actions {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
top: -0.5rem;
right: 0.25rem;
gap: 0.25rem;
color: var(--global-secondary-TextColor);
}
}
&:hover > .actions {
visibility: visible;
.title {
font-weight: 400;
color: var(--global-primary-TextColor);
min-width: 0;
margin-right: 1rem;
}
}
.notifications {
display: flex;
width: 100%;
min-width: 0;
flex-direction: column;
margin-top: var(--spacing-1);
margin-left: var(--spacing-2_5);
}
.notification {
position: relative;
.embeddedMarker {
position: absolute;
min-width: 0.25rem;
border-radius: 0;
height: 2.375rem;
background: var(--global-ui-highlight-BackgroundColor);
}
&:first-child {
.embeddedMarker {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
}
&:hover {
.embeddedMarker {
border-radius: 0.5rem;
background: var(--global-primary-LinkColor);
}
}
&:last-child {
.embeddedMarker {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
}
}
.labels {
display: flex;
flex-direction: column;
justify-content: space-between;
color: var(--global-primary-TextColor);
font-weight: 600;
font-size: 0.875rem;
gap: 0.25rem;
min-width: 0;
overflow: hidden;
margin-right: 4rem;
}
.notification {
margin-top: 0.25rem;
margin-left: 4rem;
}
.notifyMarker {
position: absolute;
left: 0.25rem;
top: 0;
&.compact {
left: 0.25rem;
top: 0.5rem;
}
.content {
display: flex;
width: 100%;
}
</style>

View File

@ -16,9 +16,9 @@
import { DocNotifyContext } from '@hcengineering/notification'
import { Doc } from '@hcengineering/core'
import { getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
import { Icon } from '@hcengineering/ui'
import { createQuery, getClient } from '@hcengineering/presentation'
import chunter from '@hcengineering/chunter'
import NotifyContextIcon from './NotifyContextIcon.svelte'
export let value: DocNotifyContext
@ -31,8 +31,6 @@
object = res[0]
})
$: icon = object && client.getHierarchy().getClass(object._class).icon
async function getTitle (object: Doc) {
if (object._class === chunter.class.DirectMessage) {
return await getDocTitle(client, object._id, object._class, object)
@ -43,9 +41,7 @@
{#if object}
<div class="flex-presenter">
{#if icon}
<Icon {icon} size="small" />
{/if}
<NotifyContextIcon {value} size="small" />
<div class="mr-4" />
{#await getTitle(object) then title}

View File

@ -1,5 +1,5 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
@ -13,90 +13,37 @@
// limitations under the License.
-->
<script lang="ts">
import { IconAdd, IconDelete, Label } from '@hcengineering/ui'
import { personAccountByIdStore, PersonAccountRefPresenter } from '@hcengineering/contact-resources'
import { Person, PersonAccount } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { DocAttributeUpdates } from '@hcengineering/activity'
import { personAccountByIdStore } from '@hcengineering/contact-resources'
import contact, { PersonAccount } from '@hcengineering/contact'
import { getCurrentAccount, Ref } from '@hcengineering/core'
import { DisplayDocUpdateMessage, DocAttributeUpdates } from '@hcengineering/activity'
import notification from '@hcengineering/notification'
import { BaseMessagePreview } from '@hcengineering/activity-resources'
import { Action, Icon, Label } from '@hcengineering/ui'
export let value: DocAttributeUpdates
export let message: DisplayDocUpdateMessage
export let actions: Action[] = []
$: removed = getAccountRefs(value.removed)
$: added = getAccountRefs(value.added.length > 0 ? value.added : value.set)
const me = getCurrentAccount()._id
function getAccountRefs (values: DocAttributeUpdates['removed' | 'added' | 'set']): Ref<PersonAccount>[] {
const persons = new Set<Ref<Person>>()
$: attributeUpdates = message.attributeUpdates ?? { added: [], removed: [], set: [] }
return values.filter((value) => {
$: isMeAdded = includeMe(attributeUpdates.added.length > 0 ? attributeUpdates.added : attributeUpdates.set)
function includeMe (values: DocAttributeUpdates['removed' | 'added' | 'set']): boolean {
return values.some((value) => {
const account = $personAccountByIdStore.get(value as Ref<PersonAccount>)
if (account === undefined) {
return false
}
if (persons.has(account.person)) {
return false
}
persons.add(account.person)
return true
}) as Ref<PersonAccount>[]
return account?._id === me
})
}
$: hasDifferentChanges = added.length > 0 && removed.length > 0
</script>
<div class="root">
<div class="label">
{#if hasDifferentChanges}
<Label label={notification.string.ChangedCollaborators} />:
{:else if added.length > 0}
<Label label={notification.string.NewCollaborators} />:
{:else if removed.length > 0}
<Label label={notification.string.RemovedCollaborators} />:
{/if}
</div>
{#if added.length > 0}
<div class="row">
{#if hasDifferentChanges}
<IconAdd size={'x-small'} fill={'var(--theme-trans-color)'} />
{/if}
{#each added as add}
<PersonAccountRefPresenter inline value={add} />
{/each}
</div>
{/if}
<div class="antiHSpacer"></div>
{#if removed.length > 0}
<div class="row">
{#if hasDifferentChanges}
<IconDelete size={'x-small'} fill={'var(--theme-trans-color)'} />
{/if}
{#each removed as remove}
<PersonAccountRefPresenter inline value={remove} />
{/each}
</div>
{/if}
</div>
<style lang="scss">
.root {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.label {
white-space: nowrap;
}
.row {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
</style>
<BaseMessagePreview {actions} {message}>
<span class="overflow-label flex-presenter flex-gap-1-5">
<Icon icon={contact.icon.Person} size="small" />
<Label
label={isMeAdded ? notification.string.YouAddedCollaborators : notification.string.YouRemovedCollaborators}
/>
</span>
</BaseMessagePreview>

View File

@ -21,10 +21,10 @@
export let kind: 'table' | 'block' = 'block'
const inboxClient = InboxNotificationsClientImpl.getClient()
const notifyContextByDocStore = inboxClient.docNotifyContextByDoc
const contextByDocStore = inboxClient.contextByDoc
const inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
$: notifyContext = $notifyContextByDocStore.get(value._id)
$: notifyContext = $contextByDocStore.get(value._id)
$: inboxNotifications = notifyContext ? $inboxNotificationsByContextStore.get(notifyContext._id) ?? [] : []
$: hasNotification = !notifyContext?.hidden && inboxNotifications.some(({ isViewed }) => !isViewed)

View File

@ -20,7 +20,12 @@
import view from '@hcengineering/view'
import { Doc } from '@hcengineering/core'
import NotifyMarker from './NotifyMarker.svelte'
export let value: DocNotifyContext
export let size: IconSize = 'medium'
export let notifyCount: number = 0
const client = getClient()
const hierarchy = client.getHierarchy()
const query = createQuery()
@ -36,10 +41,14 @@
<div class="container">
{#if iconMixin && object}
<Component is={iconMixin.component} props={{ value: object, size: 'medium' }} />
{:else}
<Icon icon={classIcon(client, value.attachedToClass) ?? notification.icon.Notifications} size="medium" />
<Component is={iconMixin.component} props={{ value: object, size }} />
{:else if !iconMixin}
<Icon icon={classIcon(client, value.attachedToClass) ?? notification.icon.Notifications} {size} />
{/if}
<div class="notifyMarker">
<NotifyMarker count={notifyCount} size="medium" />
</div>
</div>
<style lang="scss">
@ -47,10 +56,20 @@
display: flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 0.5rem;
background-color: var(--theme-button-hovered);
color: var(--global-secondary-TextColor);
border-radius: var(--medium-BorderRadius);
border: 1px solid var(--global-subtle-ui-BorderColor);
background-color: var(--global-ui-BackgroundColor);
width: 2.5rem;
height: 2.5rem;
min-width: 2.5rem;
min-height: 2.5rem;
position: relative;
.notifyMarker {
position: absolute;
top: -0.375rem;
right: -0.375rem;
}
}
</style>

View File

@ -14,12 +14,13 @@
-->
<script lang="ts">
export let count: number = 0
export let size: 'small' | 'medium' = 'small'
const maxNumber = 9
</script>
{#if count > 0}
<div class="notifyMarker">
<div class="notifyMarker {size}">
{#if count > maxNumber}
{maxNumber}+
{:else}
@ -33,12 +34,21 @@
display: flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
border-radius: 50%;
background-color: var(--highlight-red);
font-size: 0.5rem;
background-color: var(--global-higlight-Color);
color: var(--global-on-accent-TextColor);
font-weight: 700;
color: var(--white-color);
&.small {
width: 1rem;
height: 1rem;
font-size: 0.5rem;
}
&.medium {
width: 1.25rem;
height: 1.25rem;
font-size: 0.625rem;
}
}
</style>

View File

@ -0,0 +1,36 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import activity, { DisplayDocUpdateMessage, Reaction } from '@hcengineering/activity'
import { getClient } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import { BaseMessagePreview } from '@hcengineering/activity-resources'
export let message: DisplayDocUpdateMessage
const client = getClient()
let reactions: Reaction[] = []
$: void client
.findAll(activity.class.Reaction, {
_id: { $in: [message.objectId, ...(message?.previousMessages?.map((a) => a.objectId) ?? [])] as Ref<Reaction>[] }
})
.then((res) => {
reactions = res
})
</script>
<BaseMessagePreview text={reactions.map((r) => r.emoji).join('')} {message} on:click />

View File

@ -0,0 +1,104 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Icon, IconAdd, IconDelete, Label } from '@hcengineering/ui'
import { personAccountByIdStore, PersonAccountRefPresenter } from '@hcengineering/contact-resources'
import { Person, PersonAccount } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import activity, { DocAttributeUpdates } from '@hcengineering/activity'
import notification from '@hcengineering/notification'
export let value: DocAttributeUpdates
$: removed = getAccountRefs(value.removed)
$: added = getAccountRefs(value.added.length > 0 ? value.added : value.set)
function getAccountRefs (values: DocAttributeUpdates['removed' | 'added' | 'set']): Ref<PersonAccount>[] {
const persons = new Set<Ref<Person>>()
return values.filter((value) => {
const account = $personAccountByIdStore.get(value as Ref<PersonAccount>)
if (account === undefined) {
return false
}
if (persons.has(account.person)) {
return false
}
persons.add(account.person)
return true
}) as Ref<PersonAccount>[]
}
$: hasDifferentChanges = added.length > 0 && removed.length > 0
</script>
<div class="root">
<Icon icon={activity.icon.Activity} size="small" />
<div class="label">
{#if hasDifferentChanges}
<Label label={notification.string.ChangedCollaborators} />:
{:else if added.length > 0}
<Label label={notification.string.NewCollaborators} />:
{:else if removed.length > 0}
<Label label={notification.string.RemovedCollaborators} />:
{/if}
</div>
{#if added.length > 0}
<div class="row">
{#if hasDifferentChanges}
<IconAdd size={'x-small'} fill={'var(--theme-trans-color)'} />
{/if}
{#each added as add}
<PersonAccountRefPresenter value={add} avatarSize="card" />
{/each}
</div>
{/if}
<div class="antiHSpacer"></div>
{#if removed.length > 0}
<div class="row">
{#if hasDifferentChanges}
<IconDelete size={'x-small'} fill={'var(--theme-trans-color)'} />
{/if}
{#each removed as remove}
<PersonAccountRefPresenter value={remove} avatarSize="card" />
{/each}
</div>
{/if}
</div>
<style lang="scss">
.root {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.label {
white-space: nowrap;
}
.row {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
</style>

View File

@ -14,66 +14,55 @@
-->
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { matchQuery, Ref } from '@hcengineering/core'
import { matchQuery } from '@hcengineering/core'
import notification, {
ActivityInboxNotification,
ActivityNotificationViewlet,
DisplayActivityInboxNotification,
InboxNotification
DisplayActivityInboxNotification
} from '@hcengineering/notification'
import { ActivityMessagePresenter, combineActivityMessages } from '@hcengineering/activity-resources'
import {
ActivityMessagePreview,
combineActivityMessages,
sortActivityMessages
} from '@hcengineering/activity-resources'
import { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import { location, Action, Component } from '@hcengineering/ui'
import { Action, Component } from '@hcengineering/ui'
import { getActions } from '@hcengineering/view-resources'
import { getResource } from '@hcengineering/platform'
import { inboxMessagesStore, InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import { openInboxDoc } from '../../utils'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
export let value: DisplayActivityInboxNotification
export let embedded = false
export let skipLabel = false
export let showNotify = true
export let withActions = true
export let viewlets: ActivityNotificationViewlet[] = []
export let withFlatActions = false
export let onClick: (() => void) | undefined = undefined
const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient()
const notificationsStore = inboxClient.inboxNotifications
const activityNotificationsStore = inboxClient.activityInboxNotifications
let viewlet: ActivityNotificationViewlet | undefined = undefined
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
let displayMessage: DisplayActivityMessage | undefined = undefined
let actions: Action[] = []
location.subscribe((loc) => {
selectedMessageId = loc.path[4] as Ref<ActivityMessage> | undefined
})
$: combinedNotifications = $activityNotificationsStore.filter(({ _id }) => value.combinedIds.includes(_id))
$: messages = combinedNotifications
.map((it) => it.$lookup?.attachedTo)
.filter((it): it is ActivityMessage => it !== undefined)
$: combinedNotifications = $notificationsStore.filter(({ _id }) =>
(value.combinedIds as Ref<InboxNotification>[]).includes(_id)
) as ActivityInboxNotification[]
$: void updateDisplayMessage(messages)
$: messageIds = combinedNotifications.map(({ attachedTo }) => attachedTo)
$: updateDisplayMessage(messageIds, $inboxMessagesStore)
async function updateDisplayMessage (ids: Ref<ActivityMessage>[], allMessages: ActivityMessage[]) {
const messages = allMessages.filter(({ _id }) => ids.includes(_id))
const combinedMessages = await combineActivityMessages(messages)
async function updateDisplayMessage (messages: ActivityMessage[]): Promise<void> {
const combinedMessages = await combineActivityMessages(sortActivityMessages(messages))
displayMessage = combinedMessages[0]
}
$: getAllActions(value).then((res) => {
$: void getAllActions(value).then((res) => {
actions = res
})
$: updateViewlet(viewlets, displayMessage)
function updateViewlet (viewlets: ActivityNotificationViewlet[], message?: DisplayActivityMessage) {
function updateViewlet (viewlets: ActivityNotificationViewlet[], message?: DisplayActivityMessage): void {
if (viewlets.length === 0 || message === undefined) {
viewlet = undefined
return
@ -90,14 +79,6 @@
viewlet = undefined
}
function handleReply (message?: DisplayActivityMessage): void {
if (message === undefined) {
return
}
openInboxDoc(value.docNotifyContext, message._id, message._id)
}
async function getAllActions (value: ActivityInboxNotification): Promise<Action[]> {
const notificationActions = await getActions(client, value, notification.class.InboxNotification)
@ -122,31 +103,11 @@
props={{
message: displayMessage,
notification: value,
embedded,
withActions,
showNotify: showNotify ? !value.isViewed && !embedded : false,
actions,
onClick
actions
}}
on:click
/>
{:else}
<ActivityMessagePresenter
value={displayMessage}
showNotify={showNotify ? !value.isViewed && !embedded : false}
isSelected={displayMessage._id === selectedMessageId}
showEmbedded
{withActions}
{embedded}
{skipLabel}
{actions}
hoverable={false}
{withFlatActions}
videoPreload={false}
compact
onReply={() => {
handleReply(displayMessage)
}}
{onClick}
/>
<ActivityMessagePreview value={displayMessage} {actions} on:click />
{/if}
{/if}

View File

@ -13,53 +13,17 @@
// limitations under the License.
-->
<script lang="ts">
import { Employee, PersonAccount } from '@hcengineering/contact'
import {
Avatar,
SystemAvatar,
employeeByIdStore,
personAccountByIdStore,
personByIdStore,
EmployeePresenter
} from '@hcengineering/contact-resources'
import core, { Doc, getDisplayTime, Ref } from '@hcengineering/core'
import { Doc } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import notification, { CommonInboxNotification } from '@hcengineering/notification'
import { ActionIcon, IconMoreH, Label, ShowMore, showPopup } from '@hcengineering/ui'
import { getDocLinkTitle, Menu } from '@hcengineering/view-resources'
import { ActivityDocLink } from '@hcengineering/activity-resources'
import view from '@hcengineering/view'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import { getClient } from '@hcengineering/presentation'
import { CommonInboxNotification } from '@hcengineering/notification'
import { BasePreview } from '@hcengineering/activity-resources'
export let value: CommonInboxNotification
export let embedded = false
export let skipLabel = false
export let showNotify = true
export let withActions = true
export let onClick: (() => void) | undefined = undefined
const objectQuery = createQuery()
const client = getClient()
const hierarchy = client.getHierarchy()
const inboxClient = InboxNotificationsClientImpl.getClient()
const docNotifyContextsStore = inboxClient.docNotifyContexts
let isActionMenuOpened = false
let content = ''
let object: Doc | undefined = undefined
$: personAccount = $personAccountByIdStore.get((value.createdBy ?? value.modifiedBy) as Ref<PersonAccount>)
$: person =
personAccount?.person !== undefined
? $employeeByIdStore.get(personAccount.person as Ref<Employee>) ?? $personByIdStore.get(personAccount.person)
: undefined
$: context = $docNotifyContextsStore.find(({ _id }) => _id === value.docNotifyContext)
$: context &&
objectQuery.query(context.attachedToClass, { _id: context.attachedTo }, (result) => {
object = result[0]
})
$: void updateContent(value.message, value.messageHtml)
@ -71,150 +35,21 @@
}
}
function handleActionMenuOpened (): void {
isActionMenuOpened = true
}
let headerObject: Doc | undefined = undefined
function handleActionMenuClosed (): void {
isActionMenuOpened = false
}
function showMenu (ev: MouseEvent): void {
showPopup(
Menu,
{
object: value,
baseMenuClass: notification.class.InboxNotification
},
ev.target as HTMLElement,
handleActionMenuClosed
)
handleActionMenuOpened()
}
$: value.headerObjectId &&
value.headerObjectClass &&
client.findOne(value.headerObjectClass, { _id: value.headerObjectId }).then((doc) => {
headerObject = doc
})
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="root clear-mins flex-grow" on:click={onClick}>
{#if !embedded}
{#if !value.isViewed && showNotify}
<div class="notify" />
{/if}
{#if value.icon}
<SystemAvatar size="medium" icon={value.icon} iconProps={value.iconProps} />
{:else if person}
<Avatar size="medium" avatar={person.avatar} name={person.name} />
{:else}
<SystemAvatar size="medium" />
{/if}
{:else}
<div class="embeddedMarker" />
{/if}
<div class="content ml-2 w-full clear-mins">
<div class="header clear-mins">
{#if person}
<EmployeePresenter value={person} shouldShowAvatar={false} />
{:else}
<div class="strong">
<Label label={core.string.System} />
</div>
{/if}
{#if !skipLabel && value.header}
<span class="text-sm lower"><Label label={value.header} /></span>
{#if object}
{#await getDocLinkTitle(client, object._id, object._class, object) then linkTitle}
<ActivityDocLink
{object}
title={linkTitle}
panelComponent={hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectPanel)?.component}
/>
{/await}
{/if}
{/if}
<span class="text-sm">{getDisplayTime(value.createdOn ?? 0)}</span>
</div>
<div class="flex-row-center">
<div class="customContent">
<ShowMore limit={80}>
<MessageViewer message={content} />
</ShowMore>
</div>
</div>
</div>
{#if !embedded && withActions}
<div class="actions clear-mins flex flex-gap-2 items-center" class:opened={isActionMenuOpened}>
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
</div>
{/if}
</div>
<style lang="scss">
.root {
position: relative;
display: flex;
padding: 0.75rem 0.75rem 0.75rem 1rem;
border-radius: 0.5rem;
gap: 1rem;
overflow: hidden;
cursor: pointer;
.actions {
position: absolute;
visibility: hidden;
top: 0.75rem;
right: 0.75rem;
color: var(--theme-halfcontent-color);
&.opened {
visibility: visible;
}
}
.content {
padding: 0;
}
&:hover > .actions {
visibility: visible;
}
}
.header {
display: flex;
align-items: baseline;
font-size: 0.875rem;
color: var(--theme-halfcontent-color);
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: calc(100% - 3.5rem);
span {
margin-left: 0.25rem;
font-weight: 400;
line-height: 1.25rem;
}
}
.notify {
position: absolute;
top: 0.5rem;
left: 0.25rem;
height: 0.5rem;
width: 0.5rem;
background-color: var(--theme-inbox-notify);
border-radius: 50%;
}
.embeddedMarker {
width: 0.375rem;
border-radius: 0.5rem;
background: var(--secondary-button-default);
}
</style>
<BasePreview
headerIcon={value.headerIcon}
header={value.header}
{headerObject}
text={content}
account={value.createdBy ?? value.modifiedBy}
timestamp={value.createdOn ?? value.modifiedOn}
on:click
/>

View File

@ -13,13 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import notification, {
ActivityNotificationViewlet,
DisplayInboxNotification,
DocNotifyContext
} from '@hcengineering/notification'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import view, { Viewlet } from '@hcengineering/view'
import notification, { DocNotifyContext, InboxNotification } from '@hcengineering/notification'
import { ActionContext, getClient } from '@hcengineering/presentation'
import view from '@hcengineering/view'
import {
AnyComponent,
ButtonWithDropdown,
@ -27,7 +23,6 @@
defineSeparators,
IconDropdown,
Label,
Loading,
location as locationStore,
Location,
Scroller,
@ -36,23 +31,17 @@
TabList
} from '@hcengineering/ui'
import chunter, { ThreadMessage } from '@hcengineering/chunter'
import { Ref, WithLookup } from '@hcengineering/core'
import { ViewletSelector } from '@hcengineering/view-resources'
import { Account, getCurrentAccount, IdMap, Ref } from '@hcengineering/core'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { isReactionMessage } from '@hcengineering/activity-resources'
import { get } from 'svelte/store'
import { translate } from '@hcengineering/platform'
import { inboxMessagesStore, InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import Filter from '../Filter.svelte'
import {
archiveAll,
getDisplayInboxNotifications,
openInboxDoc,
readAll,
resolveLocation,
unreadAll
} from '../../utils'
import { InboxNotificationsFilter } from '../../types'
import { archiveAll, getDisplayInboxData, openInboxDoc, readAll, resolveLocation, unreadAll } from '../../utils'
import { InboxData, InboxNotificationsFilter } from '../../types'
import InboxGroupedListView from './InboxGroupedListView.svelte'
export let visibleNav: boolean = true
export let navFloat: boolean = false
@ -63,29 +52,17 @@
const inboxClient = InboxNotificationsClientImpl.getClient()
const notificationsByContextStore = inboxClient.inboxNotificationsByContext
const notifyContextsStore = inboxClient.docNotifyContexts
const messagesQuery = createQuery()
const contextByIdStore = inboxClient.contextById
const contextsStore = inboxClient.contexts
const allTab: TabItem = {
id: 'all',
labelIntl: notification.string.All
}
const channelTab: TabItem = {
id: chunter.class.Channel,
labelIntl: chunter.string.Channels
}
const directTab: TabItem = {
id: chunter.class.DirectMessage,
labelIntl: chunter.string.Direct
}
let displayNotifications: DisplayInboxNotification[] = []
let displayContextsIds = new Set<Ref<DocNotifyContext>>()
let inboxData: InboxData = new Map()
let messagesIds: Ref<ActivityMessage>[] = []
let filteredNotifications: DisplayInboxNotification[] = []
let filteredData: InboxData = new Map()
let filter: InboxNotificationsFilter = 'all'
let tabItems: TabItem[] = []
@ -95,42 +72,18 @@
let selectedContext: DocNotifyContext | undefined = undefined
let selectedComponent: AnyComponent | undefined = undefined
let viewlets: ActivityNotificationViewlet[] = []
let viewlet: WithLookup<Viewlet> | undefined
let loading = true
let selectedMessage: ActivityMessage | undefined = undefined
void client.findAll(notification.class.ActivityNotificationViewlet, {}).then((res) => {
viewlets = res
$: void getDisplayInboxData($notificationsByContextStore).then((res) => {
inboxData = res
})
$: void getDisplayInboxNotifications($notificationsByContextStore, filter).then((res) => {
displayNotifications = res
})
$: displayContextsIds = new Set(displayNotifications.map(({ docNotifyContext }) => docNotifyContext))
$: filteredNotifications = filterNotifications(selectedTabId, displayNotifications, $notifyContextsStore)
$: filteredData = filterData(filter, selectedTabId, inboxData, $contextByIdStore)
locationStore.subscribe((newLocation) => {
void syncLocation(newLocation)
})
inboxClient.activityInboxNotifications.subscribe((notifications) => {
messagesIds = notifications.map(({ attachedTo }) => attachedTo)
})
$: messagesQuery.query(
activity.class.ActivityMessage,
{
_id: { $in: messagesIds }
},
(result) => {
inboxMessagesStore.set(result)
}
)
async function syncLocation (newLocation: Location): Promise<void> {
const loc = await resolveLocation(newLocation)
@ -152,38 +105,45 @@
}
}
$: selectedContext = selectedContextId
? selectedContext ?? $notifyContextsStore.find(({ _id }) => _id === selectedContextId)
: undefined
$: selectedContext = selectedContextId ? selectedContext ?? $contextByIdStore.get(selectedContextId) : undefined
$: updateSelectedPanel(selectedContext)
$: updateTabItems(displayContextsIds, $notifyContextsStore)
$: void updateSelectedPanel(selectedContext)
$: void updateTabItems(inboxData, $contextsStore)
function updateTabItems (displayContextsIds: Set<Ref<DocNotifyContext>>, notifyContexts: DocNotifyContext[]): void {
async function updateTabItems (inboxData: InboxData, notifyContexts: DocNotifyContext[]): Promise<void> {
const displayClasses = new Set(
notifyContexts
.filter(
({ _id, attachedToClass }) =>
displayContextsIds.has(_id) && !hierarchy.isDerived(attachedToClass, activity.class.ActivityMessage)
)
.map(({ attachedToClass }) => attachedToClass)
notifyContexts.filter(({ _id }) => inboxData.has(_id)).map(({ attachedToClass }) => attachedToClass)
)
const fixedTabs = [
allTab,
displayClasses.has(chunter.class.Channel) ? channelTab : undefined,
displayClasses.has(chunter.class.DirectMessage) ? directTab : undefined
].filter((tab): tab is TabItem => tab !== undefined)
const classes = Array.from(displayClasses)
const tabs: TabItem[] = []
tabItems = fixedTabs.concat(
Array.from(displayClasses.values())
.filter((_class) => ![chunter.class.Channel, chunter.class.DirectMessage].includes(_class))
.map((_class) => ({
id: _class,
// TODO: need to get plural form
labelIntl: hierarchy.getClass(_class).label
}))
)
let messagesTab: TabItem | undefined = undefined
for (const _class of classes) {
if (hierarchy.isDerived(_class, activity.class.ActivityMessage)) {
if (messagesTab === undefined) {
messagesTab = {
id: activity.class.ActivityMessage as string,
label: await translate(activity.string.Messages, {})
}
}
continue
}
const clazz = hierarchy.getClass(_class)
const intlLabel = clazz.pluralLabel ?? clazz.label ?? _class
tabs.push({
id: _class,
label: await translate(intlLabel, {})
})
}
if (messagesTab !== undefined) {
tabs.push(messagesTab)
}
tabItems = [allTab].concat(tabs.sort((a, b) => (a.label ?? '').localeCompare(b.label ?? '')))
}
function selectTab (event: CustomEvent): void {
@ -212,10 +172,12 @@
} else if (isReactionMessage(message)) {
openInboxDoc(selectedContext._id, undefined, selectedContext.attachedTo as Ref<ActivityMessage>)
} else {
const selectedMsg = event?.detail?.notification?.attachedTo
openInboxDoc(
selectedContext._id,
selectedContext.attachedTo as Ref<ActivityMessage>,
event?.detail?.notification?.attachedTo
selectedMsg ? (selectedContext.attachedTo as Ref<ActivityMessage>) : undefined,
selectedMsg ?? (selectedContext.attachedTo as Ref<ActivityMessage>)
)
}
} else {
@ -254,23 +216,64 @@
}
function filterNotifications (
filter: InboxNotificationsFilter,
notifications: InboxNotification[]
): InboxNotification[] {
switch (filter) {
case 'unread':
return notifications.filter(({ isViewed }) => !isViewed)
case 'read':
return notifications.filter(({ isViewed }) => isViewed)
case 'all':
return notifications
}
}
function filterData (
filter: InboxNotificationsFilter,
selectedTabId: string,
displayNotifications: DisplayInboxNotification[],
notifyContexts: DocNotifyContext[]
): DisplayInboxNotification[] {
if (selectedTabId === allTab.id) {
return displayNotifications
inboxData: InboxData,
contextById: IdMap<DocNotifyContext>
): InboxData {
if (selectedTabId === allTab.id && filter === 'all') {
return inboxData
}
return displayNotifications.filter(({ docNotifyContext }) => {
const context = notifyContexts.find(({ _id }) => _id === docNotifyContext)
const result = new Map()
return context !== undefined && context.attachedToClass === selectedTabId
})
for (const [key, notifications] of inboxData) {
const resNotifications = filterNotifications(filter, notifications)
if (resNotifications.length === 0) {
continue
}
if (selectedTabId === allTab.id) {
result.set(key, resNotifications)
continue
}
const context = contextById.get(key)
if (context === undefined) {
continue
}
if (
selectedTabId === activity.class.ActivityMessage &&
hierarchy.isDerived(context.attachedToClass, activity.class.ActivityMessage)
) {
result.set(key, resNotifications)
} else if (context.attachedToClass === selectedTabId) {
result.set(key, resNotifications)
}
}
return result
}
defineSeparators('inbox', [
{ minSize: 30, maxSize: 50, size: 40, float: 'navigator' },
{ minSize: 20, maxSize: 50, size: 40, float: 'navigator' },
{ size: 'auto', minSize: 30, maxSize: 'auto', float: undefined }
])
@ -305,19 +308,12 @@
: 'landscape'} background-comp-header-color"
>
<div class="antiPanel-wrap__content">
<div class="ac-header full divide caption-height">
<div class="ac-header full divide caption-height" style:padding="0.5rem var(--spacing-1_5)">
<div class="ac-header__wrap-title mr-3">
<span class="ac-header__title"><Label label={notification.string.Inbox} /></span>
</div>
<div class="flex-grow">
<ViewletSelector
bind:viewlet
bind:loading
viewletQuery={{ attachTo: notification.class.DocNotifyContext }}
/>
<span class="title"><Label label={notification.string.Inbox} /></span>
</div>
<div class="flex flex-gap-2">
{#if displayNotifications.length > 0}
{#if inboxData.size > 0}
<ButtonWithDropdown
justify="left"
kind="regular"
@ -353,21 +349,9 @@
<TabList items={tabItems} selected={selectedTabId} on:select={selectTab} />
</div>
{#if loading || !viewlet?.$lookup?.descriptor}
<Loading />
{:else if viewlet}
<Scroller padding="1rem 0">
<Component
is={viewlet.$lookup.descriptor.component}
props={{
notifications: filteredNotifications,
viewlets,
selectedContext
}}
on:click={selectContext}
/>
</Scroller>
{/if}
<Scroller padding="0">
<InboxGroupedListView data={filteredData} selectedContext={selectedContextId} on:click={selectContext} />
</Scroller>
</div>
<Separator name="inbox" float={navFloat ? 'navigator' : true} index={0} />
</div>
@ -380,7 +364,6 @@
props={{
_id: selectedContext.attachedTo,
_class: selectedContext.attachedToClass,
embedded: true,
context: selectedContext,
activityMessage: selectedMessage,
props: { context: selectedContext }
@ -394,9 +377,16 @@
<style lang="scss">
.tabs {
display: flex;
margin: 0.5rem;
margin-top: 0.5rem;
margin-bottom: 0;
padding: 0 var(--spacing-1_5);
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--theme-navpanel-border);
}
.title {
font-weight: 600;
font-size: 1.25rem;
color: var(--global-primary-TextColor);
}
</style>

View File

@ -1,116 +0,0 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ListView } from '@hcengineering/ui'
import { ActivityNotificationViewlet, DisplayInboxNotification } from '@hcengineering/notification'
import { createEventDispatcher } from 'svelte'
import InboxNotificationPresenter from './InboxNotificationPresenter.svelte'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import { deleteInboxNotification } from '../../utils'
export let notifications: DisplayInboxNotification[] = []
export let viewlets: ActivityNotificationViewlet[] = []
const dispatch = createEventDispatcher()
const inboxClient = InboxNotificationsClientImpl.getClient()
const notifyContextsStore = inboxClient.docNotifyContexts
let list: ListView
let listSelection = 0
let element: HTMLDivElement | undefined
function onKeydown (key: KeyboardEvent): void {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection - 1)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(listSelection + 1)
}
if (key.code === 'Backspace') {
key.preventDefault()
key.stopPropagation()
const notification = notifications[listSelection]
deleteInboxNotification(notification)
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
const notification = notifications[listSelection]
const context = $notifyContextsStore.find(({ _id }) => _id === notification.docNotifyContext)
dispatch('click', {
context,
notification
})
}
}
$: if (element) {
element.focus()
}
function getNotificationKey (index: number): string {
return notifications[index]?._id ?? index.toString()
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="root" bind:this={element} tabindex="0" on:keydown={onKeydown}>
<ListView
bind:this={list}
bind:selection={listSelection}
count={notifications.length}
noScroll
colorsSchema="lumia"
lazy={true}
getKey={getNotificationKey}
>
<svelte:fragment slot="item" let:item={itemIndex}>
{@const notification = notifications[itemIndex]}
<div class="notification gap-2">
<InboxNotificationPresenter
value={notification}
{viewlets}
withFlatActions
onClick={() => {
dispatch('click', {
context: $notifyContextsStore.find(({ _id }) => _id === notification.docNotifyContext),
notification
})
}}
/>
</div>
</svelte:fragment>
</ListView>
</div>
<style lang="scss">
.root {
&:focus {
outline: 0;
}
}
.notification {
display: flex;
}
</style>

View File

@ -13,52 +13,56 @@
// limitations under the License.
-->
<script lang="ts">
import { ActivityNotificationViewlet, DisplayInboxNotification, DocNotifyContext } from '@hcengineering/notification'
import notification, {
ActivityNotificationViewlet,
DisplayInboxNotification,
DocNotifyContext
} from '@hcengineering/notification'
import { Ref } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte'
import { ListView } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import DocNotifyContextCard from '../DocNotifyContextCard.svelte'
import { deleteContextNotifications } from '../../utils'
import { InboxData } from '../../types'
export let notifications: DisplayInboxNotification[] = []
export let viewlets: ActivityNotificationViewlet[] = []
export let data: InboxData
export let selectedContext: Ref<DocNotifyContext> | undefined
const client = getClient()
const dispatch = createEventDispatcher()
const inboxClient = InboxNotificationsClientImpl.getClient()
const notifyContextsStore = inboxClient.docNotifyContexts
const contextByIdStore = inboxClient.contextById
let list: ListView
let listSelection = 0
let element: HTMLDivElement | undefined
let displayData: [Ref<DocNotifyContext>, DisplayInboxNotification[]][] = []
let viewlets: ActivityNotificationViewlet[] = []
$: updateDisplayData(notifications)
void client.findAll(notification.class.ActivityNotificationViewlet, {}).then((res) => {
viewlets = res
})
function updateDisplayData (notifications: DisplayInboxNotification[]) {
const result: [Ref<DocNotifyContext>, DisplayInboxNotification[]][] = []
$: updateDisplayData(data)
notifications.forEach((item) => {
const data = result.find(([_id]) => _id === item.docNotifyContext)
function updateDisplayData (data: InboxData): void {
displayData = Array.from(data.entries()).sort(([, notifications1], [, notifications2]) => {
const createdOn1 = notifications1[0].createdOn ?? 0
const createdOn2 = notifications2[0].createdOn ?? 0
if (!data) {
result.push([item.docNotifyContext, [item]])
} else {
data[1].push(item)
if (createdOn1 > createdOn2) {
return -1
}
if (createdOn1 < createdOn2) {
return 1
}
return 0
})
displayData = result
}
async function handleCheck (context: DocNotifyContext, isChecked: boolean) {
if (!isChecked) {
return
}
await deleteContextNotifications(context)
}
function onKeydown (key: KeyboardEvent): void {
@ -76,30 +80,29 @@
key.preventDefault()
key.stopPropagation()
const context = $notifyContextsStore.find(({ _id }) => _id === displayData[listSelection]?.[0])
const contextId = displayData[listSelection]?.[0]
const context = $contextByIdStore.get(contextId)
void deleteContextNotifications(context)
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
const context = $notifyContextsStore.find(({ _id }) => _id === displayData[listSelection]?.[0])
const contextId = displayData[listSelection]?.[0]
const context = $contextByIdStore.get(contextId)
dispatch('click', { context })
}
}
$: if (element) {
$: if (element != null) {
element.focus()
}
function getContextKey (index: number): string {
const contextId = displayData[index][0]
if (contextId === undefined) {
return index.toString()
}
return contextId
return contextId ?? index.toString()
}
</script>
@ -110,7 +113,9 @@
bind:this={list}
bind:selection={listSelection}
count={displayData.length}
highlightIndex={displayData.findIndex(([context]) => context === selectedContext)}
noScroll
kind="full-size"
colorsSchema="lumia"
lazy={true}
getKey={getContextKey}
@ -118,21 +123,17 @@
<svelte:fragment slot="item" let:item={itemIndex}>
{@const contextId = displayData[itemIndex][0]}
{@const contextNotifications = displayData[itemIndex][1]}
{@const context = $notifyContextsStore.find(({ _id }) => _id === contextId)}
{@const context = $contextByIdStore.get(contextId)}
{#if context}
<DocNotifyContextCard
value={context}
visibleNotification={contextNotifications[0]}
isCompact={contextNotifications.length === 1}
unreadCount={contextNotifications.filter(({ isViewed }) => !isViewed).length}
notifications={contextNotifications}
{viewlets}
on:click={(event) => {
dispatch('click', event.detail)
listSelection = itemIndex
}}
on:check={(event) => handleCheck(context, event.detail)}
/>
<div class="separator" />
{/if}
</svelte:fragment>
</ListView>
@ -144,10 +145,4 @@
outline: 0;
}
}
.separator {
width: 100%;
height: 1px;
background-color: var(--theme-navpanel-border);
}
</style>

View File

@ -20,14 +20,7 @@
import { ActivityNotificationViewlet, DisplayInboxNotification } from '@hcengineering/notification'
export let value: DisplayInboxNotification
export let embedded = false
export let skipLabel = false
export let showNotify = true
export let withActions = true
export let viewlets: ActivityNotificationViewlet[] = []
export let withFlatActions = false
export let onClick: (() => void) | undefined = undefined
export let onCheck: ((isChecked: boolean) => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -36,8 +29,5 @@
</script>
{#if objectPresenter}
<Component
is={objectPresenter.presenter}
props={{ value, embedded, skipLabel, viewlets, showNotify, withActions, withFlatActions, onClick, onCheck }}
/>
<Component is={objectPresenter.presenter} props={{ value, viewlets }} on:click />
{/if}

View File

@ -21,7 +21,9 @@ import {
type Ref,
type TxOperations,
type WithLookup,
generateId
generateId,
toIdMap,
type IdMap
} from '@hcengineering/core'
import notification, {
type ActivityInboxNotification,
@ -33,16 +35,19 @@ import notification, {
import { createQuery, getClient } from '@hcengineering/presentation'
import { derived, get, writable } from 'svelte/store'
export const inboxMessagesStore = writable<ActivityMessage[]>([])
/**
* @public
*/
export class InboxNotificationsClientImpl implements InboxNotificationsClient {
protected static _instance: InboxNotificationsClientImpl | undefined = undefined
readonly docNotifyContexts = writable<DocNotifyContext[]>([])
readonly docNotifyContextByDoc = writable<Map<Ref<Doc>, DocNotifyContext>>(new Map())
readonly contexts = writable<DocNotifyContext[]>([])
readonly contextByDoc = writable<Map<Ref<Doc>, DocNotifyContext>>(new Map())
readonly contextById = derived(
[this.contexts],
([contexts]) => toIdMap(contexts),
new Map() as IdMap<DocNotifyContext>
)
readonly activityInboxNotifications = writable<Array<WithLookup<ActivityInboxNotification>>>([])
readonly otherInboxNotifications = writable<InboxNotification[]>([])
@ -58,7 +63,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
)
readonly inboxNotificationsByContext = derived(
[this.docNotifyContexts, this.inboxNotifications],
[this.contexts, this.inboxNotifications],
([notifyContexts, inboxNotifications]) => {
if (inboxNotifications.length === 0 || notifyContexts.length === 0) {
return new Map<Ref<DocNotifyContext>, InboxNotification[]>()
@ -76,22 +81,22 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
}
)
private readonly docNotifyContextsQuery = createQuery(true)
private readonly contextsQuery = createQuery(true)
private readonly otherInboxNotificationsQuery = createQuery(true)
private readonly activityInboxNotificationsQuery = createQuery(true)
private _docNotifyContextByDoc = new Map<Ref<Doc>, DocNotifyContext>()
private _contextByDoc = new Map<Ref<Doc>, DocNotifyContext>()
private constructor () {
this.docNotifyContextsQuery.query(
this.contextsQuery.query(
notification.class.DocNotifyContext,
{
user: getCurrentAccount()._id
},
(result: DocNotifyContext[]) => {
this.docNotifyContexts.set(result)
this._docNotifyContextByDoc = new Map(result.map((updates) => [updates.attachedTo, updates]))
this.docNotifyContextByDoc.set(this._docNotifyContextByDoc)
this.contexts.set(result)
this._contextByDoc = new Map(result.map((updates) => [updates.attachedTo, updates]))
this.contextByDoc.set(this._contextByDoc)
}
)
this.otherInboxNotificationsQuery.query(
@ -143,7 +148,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
}
async readDoc (client: TxOperations, _id: Ref<Doc>): Promise<void> {
const docNotifyContext = this._docNotifyContextByDoc.get(_id)
const docNotifyContext = this._contextByDoc.get(_id)
if (docNotifyContext === undefined) {
return
@ -160,7 +165,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
}
async forceReadDoc (client: TxOperations, _id: Ref<Doc>, _class: Ref<Class<Doc>>): Promise<void> {
const context = this._docNotifyContextByDoc.get(_id)
const context = this._contextByDoc.get(_id)
if (context !== undefined) {
await this.readDoc(client, _id)
@ -265,7 +270,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
},
{ projection: { _id: 1, _class: 1, space: 1 } }
)
const contexts = get(this.docNotifyContexts) ?? []
const contexts = get(this.contexts) ?? []
for (const notification of inboxNotifications) {
await ops.removeDoc(notification._class, notification.space, notification._id)
}
@ -292,7 +297,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
},
{ projection: { _id: 1, _class: 1, space: 1 } }
)
const contexts = get(this.docNotifyContexts) ?? []
const contexts = get(this.contexts) ?? []
for (const notification of inboxNotifications) {
await ops.updateDoc(notification._class, notification.space, notification._id, { isViewed: true })
}
@ -318,7 +323,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
},
{ projection: { _id: 1, _class: 1, space: 1 } }
)
const contexts = get(this.docNotifyContexts) ?? []
const contexts = get(this.contexts) ?? []
for (const notification of inboxNotifications) {
await ops.updateDoc(notification._class, notification.space, notification._id, { isViewed: false })

View File

@ -22,28 +22,18 @@ import NotificationPresenter from './components/NotificationPresenter.svelte'
import TxCollaboratorsChange from './components/activity/TxCollaboratorsChange.svelte'
import TxDmCreation from './components/activity/TxDmCreation.svelte'
import DocNotifyContextPresenter from './components/DocNotifyContextPresenter.svelte'
import NotificationCollaboratorsChanged from './components/NotificationCollaboratorsChanged.svelte'
import CollaboratorsChanged from './components/activity/CollaboratorsChanged.svelte'
import ActivityInboxNotificationPresenter from './components/inbox/ActivityInboxNotificationPresenter.svelte'
import CommonInboxNotificationPresenter from './components/inbox/CommonInboxNotificationPresenter.svelte'
import InboxFlatListView from './components/inbox/InboxFlatListView.svelte'
import InboxGroupedListView from './components/inbox/InboxGroupedListView.svelte'
import NotificationCollaboratorsChanged from './components/NotificationCollaboratorsChanged.svelte'
import ReactionNotificationPresenter from './components/ReactionNotificationPresenter.svelte'
import {
unsubscribe,
resolveLocation,
markAsReadInboxNotification,
markAsUnreadInboxNotification,
deleteInboxNotification,
hasMarkAsUnreadAction,
hasMarkAsReadAction,
hasDocNotifyContextPinAction,
isDocNotifyContextHidden,
hasDocNotifyContextUnpinAction,
isDocNotifyContextVisible,
hasHiddenDocNotifyContext,
pinDocNotifyContext,
unpinDocNotifyContext,
hideDocNotifyContext,
unHideDocNotifyContext,
canReadNotifyContext,
canUnReadNotifyContext,
readNotifyContext,
@ -68,40 +58,30 @@ export default async (): Promise<Resources> => ({
Inbox,
NotificationPresenter,
NotificationSettings,
NotificationCollaboratorsChanged,
CollaboratorsChanged,
DocNotifyContextPresenter,
ActivityInboxNotificationPresenter,
CommonInboxNotificationPresenter,
InboxFlatListView,
InboxGroupedListView
NotificationCollaboratorsChanged,
ReactionNotificationPresenter
},
activity: {
TxCollaboratorsChange,
TxDmCreation
},
function: {
HasMarkAsUnreadAction: hasMarkAsUnreadAction,
HasMarkAsReadAction: hasMarkAsReadAction,
// eslint-disable-next-line @typescript-eslint/unbound-method
GetInboxNotificationsClient: InboxNotificationsClientImpl.getClient,
HasDocNotifyContextPinAction: hasDocNotifyContextPinAction,
HasDocNotifyContextUnpinAction: hasDocNotifyContextUnpinAction,
IsDocNotifyContextHidden: isDocNotifyContextHidden,
IsDocNotifyContextTracked: isDocNotifyContextVisible,
HasHiddenDocNotifyContext: hasHiddenDocNotifyContext,
CanReadNotifyContext: canReadNotifyContext,
CanUnReadNotifyContext: canUnReadNotifyContext,
HasInboxNotifications: hasInboxNotifications
},
actionImpl: {
Unsubscribe: unsubscribe,
MarkAsReadInboxNotification: markAsReadInboxNotification,
MarkAsUnreadInboxNotification: markAsUnreadInboxNotification,
DeleteInboxNotification: deleteInboxNotification,
PinDocNotifyContext: pinDocNotifyContext,
UnpinDocNotifyContext: unpinDocNotifyContext,
HideDocNotifyContext: hideDocNotifyContext,
UnHideDocNotifyContext: unHideDocNotifyContext,
ReadNotifyContext: readNotifyContext,
UnReadNotifyContext: unReadNotifyContext,
DeleteContextNotifications: deleteContextNotifications,

View File

@ -29,7 +29,6 @@ export default mergeIds(notificationId, notification, {
MarkAllAsRead: '' as IntlString,
Change: '' as IntlString,
AddedRemoved: '' as IntlString,
YouAddedCollaborators: '' as IntlString,
YouHaveJoinedTheConversation: '' as IntlString,
ChangeCollaborators: '' as IntlString,
Activity: '' as IntlString,

View File

@ -1 +1,6 @@
import type { Ref } from '@hcengineering/core'
import type { DisplayInboxNotification, DocNotifyContext } from '@hcengineering/notification'
export type InboxNotificationsFilter = 'all' | 'read' | 'unread'
export type InboxData = Map<Ref<DocNotifyContext>, DisplayInboxNotification[]>

View File

@ -33,7 +33,6 @@ import notification, {
notificationId,
type ActivityInboxNotification,
type Collaborators,
type DisplayActivityInboxNotification,
type DisplayInboxNotification,
type DocNotifyContext,
type InboxNotification
@ -43,157 +42,7 @@ import { getLocation, navigate, type Location, type ResolvedLocation, showPopup
import { get } from 'svelte/store'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
import { type InboxNotificationsFilter } from './types'
/**
* @public
*/
export async function hasMarkAsReadAction (doc: DisplayInboxNotification): Promise<boolean> {
const inboxNotificationsClient = InboxNotificationsClientImpl.getClient()
const combinedIds =
doc._class === notification.class.ActivityInboxNotification
? (doc as DisplayActivityInboxNotification).combinedIds
: [doc._id]
return get(inboxNotificationsClient.inboxNotifications).some(
({ _id, isViewed }) => combinedIds.includes(_id) && !isViewed
)
}
/**
* @public
*/
export async function hasMarkAsUnreadAction (doc: DisplayInboxNotification): Promise<boolean> {
const canRead = await hasMarkAsReadAction(doc)
return !canRead
}
/**
* @public
*/
export async function markAsReadInboxNotification (doc: DisplayInboxNotification): Promise<void> {
const notificationsClient = InboxNotificationsClientImpl.getClient()
const isActivityNotification = doc._class === notification.class.ActivityInboxNotification
const ids = (isActivityNotification ? (doc as DisplayActivityInboxNotification).combinedIds : [doc._id]) ?? []
if (isActivityNotification) {
await updateLastViewedTimestampOnRead(doc as WithLookup<ActivityInboxNotification>, ids)
}
const doneOp = await getClient().measure('markAsRead')
const ops = getClient().apply(doc._id)
try {
await notificationsClient.readNotifications(ops, ids)
} finally {
await ops.commit()
await doneOp()
}
}
async function updateLastViewedTimestampOnRead (
doc: WithLookup<ActivityInboxNotification>,
viewedIds: Array<Ref<InboxNotification>>
): Promise<void> {
const notificationsClient = InboxNotificationsClientImpl.getClient()
const client = getClient()
const context = get(notificationsClient.docNotifyContexts).find(({ _id }) => _id === doc.docNotifyContext)
if (context === undefined) {
return
}
const unViewed = get(notificationsClient.activityInboxNotifications).filter(
({ _id, isViewed, docNotifyContext }) => context._id === docNotifyContext && !isViewed && !viewedIds.includes(_id)
)
let lastViewedTimestamp = context?.lastViewedTimestamp
if (unViewed.length === 0) {
lastViewedTimestamp = doc?.$lookup?.attachedTo?.createdOn ?? context.lastViewedTimestamp
} else {
const firstUnViewed = unViewed[unViewed.length - 1]
const hasNotificationsBefore = (firstUnViewed.createdOn ?? 0) < (doc.createdOn ?? 0)
if (!hasNotificationsBefore) {
lastViewedTimestamp = doc?.$lookup?.attachedTo?.createdOn ?? context.lastViewedTimestamp
}
}
if (lastViewedTimestamp !== undefined && lastViewedTimestamp > (context.lastViewedTimestamp ?? 0)) {
await client.update(context, { lastViewedTimestamp })
}
}
async function updateLastViewedOnUnread (doc: WithLookup<ActivityInboxNotification>): Promise<void> {
const notificationsClient = InboxNotificationsClientImpl.getClient()
const client = getClient()
const context = get(notificationsClient.docNotifyContexts).find(({ _id }) => _id === doc.docNotifyContext)
if (context === undefined) {
return
}
const messageTimestamp = doc?.$lookup?.attachedTo?.createdOn
if (messageTimestamp === undefined || messageTimestamp === 0) {
return
}
const lastViewedTimestamp = messageTimestamp - 1
if (lastViewedTimestamp < (context.lastViewedTimestamp ?? 0)) {
await client.update(context, { lastViewedTimestamp })
}
}
/**
* @public
*/
export async function markAsUnreadInboxNotification (doc: DisplayInboxNotification): Promise<void> {
const inboxNotificationsClient = InboxNotificationsClientImpl.getClient()
const isActivityNotification = doc._class === notification.class.ActivityInboxNotification
const ids = isActivityNotification ? (doc as DisplayActivityInboxNotification).combinedIds : [doc._id]
if (isActivityNotification) {
await updateLastViewedOnUnread(doc as WithLookup<ActivityInboxNotification>)
}
const doneOp = await getClient().measure('unreadNotifications')
const ops = getClient().apply(doc._id)
try {
await inboxNotificationsClient.unreadNotifications(ops, ids)
} finally {
await ops.commit()
await doneOp()
}
}
export async function deleteInboxNotification (doc: DisplayInboxNotification): Promise<void> {
const inboxNotificationsClient = InboxNotificationsClientImpl.getClient()
const isActivityNotification = doc._class === notification.class.ActivityInboxNotification
const ids = isActivityNotification ? (doc as DisplayActivityInboxNotification).combinedIds : [doc._id]
if (isActivityNotification) {
await updateLastViewedTimestampOnRead(doc as WithLookup<ActivityInboxNotification>, ids)
}
const doneOp = await getClient().measure('deleteNotifications')
const ops = getClient().apply(doc._id)
try {
await inboxNotificationsClient.deleteNotifications(ops, ids)
} finally {
await ops.commit()
await doneOp()
}
}
import { type InboxData, type InboxNotificationsFilter } from './types'
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
if (docNotifyContext.hidden) {
@ -209,29 +58,6 @@ export async function hasDocNotifyContextUnpinAction (docNotifyContext: DocNotif
return docNotifyContext.isPinned === true
}
export async function hasHiddenDocNotifyContext (contexts: DocNotifyContext[]): Promise<boolean> {
return contexts.some(({ hidden }) => hidden)
}
export async function hideDocNotifyContext (notifyContext: DocNotifyContext): Promise<void> {
const client = getClient()
await client.update(notifyContext, { hidden: true })
await deleteContextNotifications(notifyContext)
}
export async function unHideDocNotifyContext (notifyContext: DocNotifyContext): Promise<void> {
const client = getClient()
await client.update(notifyContext, { hidden: false, lastViewedTimestamp: Date.now() })
}
export async function isDocNotifyContextHidden (notifyContext: DocNotifyContext): Promise<boolean> {
return notifyContext.hidden
}
export async function isDocNotifyContextVisible (notifyContext: DocNotifyContext): Promise<boolean> {
return !notifyContext.hidden
}
/**
* @public
*/
@ -280,9 +106,9 @@ export async function readNotifyContext (doc: DocNotifyContext): Promise<void> {
export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void> {
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
const notificationToUnread = inboxNotifications[0]
const notificationsToUnread = inboxNotifications.filter(({ isViewed }) => isViewed)
if (notificationToUnread === undefined) {
if (notificationsToUnread.length === 0) {
return
}
@ -290,11 +116,14 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
const ops = getClient().apply(doc._id)
try {
await inboxClient.unreadNotifications(ops, [notificationToUnread._id])
await inboxClient.unreadNotifications(
ops,
notificationsToUnread.map(({ _id }) => _id)
)
const toUnread = inboxNotifications.find(isActivityNotification)
if (notificationToUnread._class === notification.class.ActivityInboxNotification) {
const activityNotification = notificationToUnread as WithLookup<ActivityInboxNotification>
const createdOn = activityNotification?.$lookup?.attachedTo?.createdOn
if (toUnread !== undefined) {
const createdOn = (toUnread as WithLookup<ActivityInboxNotification>)?.$lookup?.attachedTo?.createdOn
if (createdOn === undefined || createdOn === 0) {
return
@ -316,16 +145,19 @@ export async function deleteContextNotifications (doc?: DocNotifyContext): Promi
return
}
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
const doneOp = await getClient().measure('deleteContextNotifications')
const ops = getClient().apply(doc._id)
try {
await inboxClient.deleteNotifications(
ops,
inboxNotifications.map(({ _id }) => _id)
const notifications = await ops.findAll(
notification.class.InboxNotification,
{ docNotifyContext: doc._id },
{ projection: { _id: 1, _class: 1, space: 1 } }
)
for (const notification of notifications) {
await ops.removeDoc(notification._class, notification.space, notification._id)
}
await ops.update(doc, { lastViewedTimestamp: Date.now() })
} finally {
await ops.commit()
@ -442,32 +274,33 @@ export async function unreadAll (): Promise<void> {
await client.unreadAllNotifications()
}
export function isActivityNotification (doc: InboxNotification): doc is ActivityInboxNotification {
return doc._class === notification.class.ActivityInboxNotification
}
export async function getDisplayInboxNotifications (
notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>,
notifications: Array<WithLookup<InboxNotification>>,
filter: InboxNotificationsFilter = 'all',
objectClass?: Ref<Class<Doc>>
): Promise<DisplayInboxNotification[]> {
const filteredNotifications = Array.from(notificationsByContext.values())
.flat()
.filter(({ isViewed }) => {
switch (filter) {
case 'all':
return true
case 'unread':
return !isViewed
case 'read':
return !!isViewed
default:
return false
}
})
const result: DisplayInboxNotification[] = []
const activityNotifications: Array<WithLookup<ActivityInboxNotification>> = []
const activityNotifications = filteredNotifications.filter(
(n): n is WithLookup<ActivityInboxNotification> => n._class === notification.class.ActivityInboxNotification
)
const displayNotifications: DisplayInboxNotification[] = filteredNotifications.filter(
({ _class }) => _class !== notification.class.ActivityInboxNotification
)
for (const notification of notifications) {
if (filter === 'unread' && notification.isViewed) {
continue
}
if (filter === 'read' && !notification.isViewed) {
continue
}
if (isActivityNotification(notification)) {
activityNotifications.push(notification)
} else {
result.push(notification)
}
}
const messages: ActivityMessage[] = activityNotifications
.map((activityNotification) => activityNotification.$lookup?.attachedTo)
@ -505,11 +338,11 @@ export async function getDisplayInboxNotifications (
combinedIds: activityNotifications.filter(({ attachedTo }) => ids.includes(attachedTo)).map(({ _id }) => _id)
}
displayNotifications.push(displayNotification)
result.push(displayNotification)
} else {
const activityNotification = activityNotifications.find(({ attachedTo }) => attachedTo === message._id)
if (activityNotification !== undefined) {
displayNotifications.push({
result.push({
...activityNotification,
combinedIds: [activityNotification._id]
})
@ -517,18 +350,38 @@ export async function getDisplayInboxNotifications (
}
}
return displayNotifications.sort(
return result.sort(
(notification1, notification2) =>
(notification2.createdOn ?? notification2.modifiedOn) - (notification1.createdOn ?? notification1.modifiedOn)
)
}
export async function getDisplayInboxData (
notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>,
filter: InboxNotificationsFilter = 'all',
objectClass?: Ref<Class<Doc>>
): Promise<InboxData> {
const result: InboxData = new Map()
for (const key of notificationsByContext.keys()) {
const notifications = notificationsByContext.get(key) ?? []
const displayNotifications = await getDisplayInboxNotifications(notifications, filter, objectClass)
if (displayNotifications.length > 0) {
result.set(key, displayNotifications)
}
}
return result
}
export async function hasInboxNotifications (
notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>
): Promise<boolean> {
const displayNotifications = await getDisplayInboxNotifications(notificationsByContext)
const unreadInboxData = await getDisplayInboxData(notificationsByContext, 'unread')
return displayNotifications.some(({ isViewed }) => !isViewed)
return unreadInboxData.size > 0
}
export async function getNotificationsCount (
@ -539,9 +392,9 @@ export async function getNotificationsCount (
return 0
}
const displayNotifications = await getDisplayInboxNotifications(new Map([[context._id, notifications]]))
const unreadNotifications = await getDisplayInboxNotifications(notifications, 'unread')
return displayNotifications.filter(({ isViewed }) => !isViewed).length
return unreadNotifications.length
}
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
@ -576,7 +429,6 @@ async function generateLocation (
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
const threadId = loc.path[4] as Ref<ActivityMessage> | undefined
const messageId = loc.query?.message as Ref<ActivityMessage> | undefined
const contextNotification = await client.findOne(notification.class.InboxNotification, {
docNotifyContext: contextId
@ -596,21 +448,19 @@ async function generateLocation (
}
const thread =
threadId !== undefined ? await client.findOne(activity.class.ActivityMessage, { _id: messageId }) : undefined
const message =
messageId !== undefined ? await client.findOne(activity.class.ActivityMessage, { _id: messageId }) : undefined
threadId !== undefined ? await client.findOne(activity.class.ActivityMessage, { _id: threadId }) : undefined
if (thread === undefined) {
return {
loc: {
path: [appComponent, workspace, notificationId, contextId],
fragment: undefined,
query: { ...loc.query, message: message !== undefined ? (messageId as string) : null }
query: { ...loc.query }
},
defaultLocation: {
path: [appComponent, workspace, notificationId, contextId],
fragment: undefined,
query: { ...loc.query, message: message !== undefined ? (messageId as string) : null }
query: { ...loc.query }
}
}
}
@ -619,12 +469,12 @@ async function generateLocation (
loc: {
path: [appComponent, workspace, notificationId, contextId, threadId as string],
fragment: undefined,
query: { ...loc.query, message: message !== undefined ? (messageId as string) : null }
query: { ...loc.query }
},
defaultLocation: {
path: [appComponent, workspace, notificationId, contextId, threadId as string],
fragment: undefined,
query: { ...loc.query, message: message !== undefined ? (messageId as string) : null }
query: { ...loc.query }
}
}
}
@ -651,6 +501,7 @@ export function openInboxDoc (
if (thread !== undefined) {
loc.path[4] = thread
loc.path.length = 5
} else {
loc.path[4] = ''
loc.path.length = 4

View File

@ -21,6 +21,7 @@ import {
Class,
Doc,
DocumentQuery,
IdMap,
Mixin,
Ref,
Space,
@ -34,7 +35,7 @@ import { plugin } from '@hcengineering/platform'
import { Preference } from '@hcengineering/preference'
import { IntegrationType } from '@hcengineering/setting'
import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui'
import { Action, Viewlet, ViewletDescriptor } from '@hcengineering/view'
import { Action } from '@hcengineering/view'
import { Readable, Writable } from './types'
export * from './types'
@ -238,6 +239,9 @@ export interface ActivityInboxNotification extends InboxNotification {
export interface CommonInboxNotification extends InboxNotification {
header?: IntlString
headerIcon?: Asset
headerObjectId?: Ref<Doc>
headerObjectClass?: Ref<Class<Doc>>
message?: IntlString
messageHtml?: string
props?: Record<string, any>
@ -271,11 +275,14 @@ export interface DocNotifyContext extends Doc {
* @public
*/
export interface InboxNotificationsClient {
docNotifyContextByDoc: Writable<Map<Ref<Doc>, DocNotifyContext>>
docNotifyContexts: Writable<DocNotifyContext[]>
contextByDoc: Writable<Map<Ref<Doc>, DocNotifyContext>>
contexts: Writable<DocNotifyContext[]>
contextById: Readable<IdMap<DocNotifyContext>>
inboxNotifications: Readable<InboxNotification[]>
activityInboxNotifications: Writable<ActivityInboxNotification[]>
inboxNotificationsByContext: Readable<Map<Ref<DocNotifyContext>, InboxNotification[]>>
readDoc: (client: TxOperations, _id: Ref<Doc>) => Promise<void>
forceReadDoc: (client: TxOperations, _id: Ref<Doc>, _class: Ref<Class<Doc>>) => Promise<void>
readMessages: (client: TxOperations, ids: Ref<ActivityMessage>[]) => Promise<void>
@ -344,28 +351,17 @@ const notification = plugin(notificationId, {
component: {
Inbox: '' as AnyComponent,
NotificationPresenter: '' as AnyComponent,
NotificationCollaboratorsChanged: '' as AnyComponent,
CollaboratorsChanged: '' as AnyComponent,
DocNotifyContextPresenter: '' as AnyComponent,
InboxFlatListView: '' as AnyComponent,
InboxGroupedListView: '' as AnyComponent
NotificationCollaboratorsChanged: '' as AnyComponent,
ReactionNotificationPresenter: '' as AnyComponent
},
activity: {
TxCollaboratorsChange: '' as AnyComponent
},
viewlet: {
FlatList: '' as Ref<ViewletDescriptor>,
InboxFlatList: '' as Ref<Viewlet>,
GroupedList: '' as Ref<ViewletDescriptor>,
InboxGroupedList: '' as Ref<Viewlet>
},
action: {
MarkAsUnreadInboxNotification: '' as Ref<Action>,
MarkAsReadInboxNotification: '' as Ref<Action>,
DeleteInboxNotification: '' as Ref<Action>,
PinDocNotifyContext: '' as Ref<Action>,
UnpinDocNotifyContext: '' as Ref<Action>,
HideDocNotifyContext: '' as Ref<Action>,
UnHideDocNotifyContext: '' as Ref<Action>,
UnReadNotifyContext: '' as Ref<Action>,
ReadNotifyContext: '' as Ref<Action>,
DeleteContextNotifications: '' as Ref<Action>
@ -394,20 +390,17 @@ const notification = plugin(notificationId, {
RemovedCollaborators: '' as IntlString,
Edited: '' as IntlString,
Pinned: '' as IntlString,
FlatList: '' as IntlString,
GroupedList: '' as IntlString,
All: '' as IntlString,
ArchiveAll: '' as IntlString,
MarkReadAll: '' as IntlString,
MarkUnreadAll: '' as IntlString,
ArchiveAllConfirmationTitle: '' as IntlString,
ArchiveAllConfirmationMessage: '' as IntlString
ArchiveAllConfirmationMessage: '' as IntlString,
YouAddedCollaborators: '' as IntlString,
YouRemovedCollaborators: '' as IntlString
},
function: {
GetInboxNotificationsClient: '' as Resource<InboxNotificationsClientFactory>,
HasHiddenDocNotifyContext: '' as Resource<(doc: Doc[]) => Promise<boolean>>,
IsDocNotifyContextHidden: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
IsDocNotifyContextTracked: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasInboxNotifications: '' as Resource<
(notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>
>

View File

@ -22,6 +22,7 @@
export let _id: Ref<TelegramMessage> | undefined = undefined
export let value: TelegramMessage | undefined = undefined
export let preview = false
const query = createQuery()
const client = getClient()
@ -42,8 +43,8 @@
</script>
{#if value}
<div class="content lines-limit-2">
<MessageViewer message={value.content} />
<div class="content lines-limit-2 overflow-label">
<MessageViewer message={value.content} {preview} />
</div>
{/if}

View File

@ -138,6 +138,7 @@ export default plugin(timeId, {
DayCalendar: '' as IntlString,
CreatedToDo: '' as IntlString,
AddToDo: '' as IntlString,
NewToDoDetails: '' as IntlString
NewToDoDetails: '' as IntlString,
ToDo: '' as IntlString
}
})

View File

@ -0,0 +1,35 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { taskTypeStore } from '@hcengineering/task-resources'
import { Issue } from '@hcengineering/tracker'
import { IconSize } from '@hcengineering/ui'
import { getTaskTypeStates } from '@hcengineering/task'
import { statusStore } from '@hcengineering/view-resources'
import IssueStatusIcon from './IssueStatusIcon.svelte'
export let value: Issue | undefined
export let size: IconSize = 'small'
$: statuses = value ? getTaskTypeStates(value.kind, $taskTypeStore, $statusStore.byId) : []
$: issueStatus = statuses?.find((status) => status._id === value?.status) ?? statuses[0]
</script>
{#if value}
<IssueStatusIcon value={issueStatus} {size} space={value.space} />
{/if}

View File

@ -85,6 +85,7 @@ import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.sv
import SettingsRelatedTargets from './components/SettingsRelatedTargets.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
import IssueExtra from './components/issues/IssueExtra.svelte'
import IssueStatusPresenter from './components/issues/IssueStatusPresenter.svelte'
import {
getIssueTitle,
getTitle,
@ -510,7 +511,8 @@ export default async (): Promise<Resources> => ({
PriorityIconPresenter,
IssueSearchIcon,
MembersArrayEditor,
IssueExtra
IssueExtra,
IssueStatusPresenter
},
completion: {
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>

Some files were not shown because too many files have changed in this diff Show More