mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-6124: Rework inbox view (#5046)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
9f198a5428
commit
3bbb8915ec
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
},
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
|
@ -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); }
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -38,6 +38,8 @@
|
||||
"Mentioned": "Mentioned",
|
||||
"You": "You",
|
||||
"Mentions": "Mentions",
|
||||
"MentionedYouIn": "Mentioned you in"
|
||||
"MentionedYouIn": "Mentioned you in",
|
||||
"Messages": "Messages",
|
||||
"Thread": "Thread"
|
||||
}
|
||||
}
|
@ -38,6 +38,8 @@
|
||||
"Mentioned": "Упомянул(а)",
|
||||
"You": "Вы",
|
||||
"Mentions": "Упоминания",
|
||||
"MentionedYouIn": "Упомянул(а) вас в"
|
||||
"MentionedYouIn": "Упомянул(а) вас в",
|
||||
"Messages": "Cообщения",
|
||||
"Thread": "Обсуждение"
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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} />
|
||||
|
@ -69,9 +69,6 @@
|
||||
<PinMessageAction object={message} />
|
||||
<SaveMessageAction object={message} />
|
||||
{/if}
|
||||
{#if withActionMenu}
|
||||
<ActivityMessageAction icon={IconMoreV} action={showMenu} opened={isActionMenuOpened} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
250
plugins/activity-resources/src/components/BasePreview.svelte
Normal file
250
plugins/activity-resources/src/components/BasePreview.svelte
Normal 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>
|
@ -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>
|
@ -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
|
@ -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} />
|
@ -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>
|
||||
|
@ -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}
|
@ -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 {
|
||||
|
@ -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>
|
@ -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 />
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
@ -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>
|
||||
|
@ -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>
|
@ -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,
|
||||
|
@ -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>,
|
||||
|
@ -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}
|
@ -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 {
|
||||
|
@ -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>
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -62,7 +62,6 @@
|
||||
dispatch('changeContent')
|
||||
}}
|
||||
on:keydown={(evt) => {
|
||||
console.log(evt)
|
||||
if (isTextMode) {
|
||||
evt.preventDefault()
|
||||
evt.stopImmediatePropagation()
|
||||
|
@ -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>
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 &&
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -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 />
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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, {
|
||||
|
@ -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>>,
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -109,7 +109,7 @@ export async function filterChannelHasNewMessagesResult (
|
||||
filter,
|
||||
onUpdate,
|
||||
undefined,
|
||||
get(inboxClient.docNotifyContextByDoc),
|
||||
get(inboxClient.contextByDoc),
|
||||
get(inboxClient.inboxNotificationsByContext)
|
||||
)
|
||||
return { $in: result }
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +33,6 @@
|
||||
"RemovedCollaborators": "Colaboradores Eliminados",
|
||||
"Edited": "Editado",
|
||||
"Pinned": "Fijado",
|
||||
"Message": "Mensaje",
|
||||
"FlatList": "Lista Plana",
|
||||
"GroupedList": "Lista Agrupada"
|
||||
"Message": "Mensaje"
|
||||
}
|
||||
}
|
@ -33,8 +33,6 @@
|
||||
"RemovedCollaborators": "Colaboradores removidos",
|
||||
"Edited": "Editado",
|
||||
"Pinned": "Fixado",
|
||||
"Message": "Mensagem",
|
||||
"FlatList": "Lista Plana",
|
||||
"GroupedList": "Lista Agrupada"
|
||||
"Message": "Mensagem"
|
||||
}
|
||||
}
|
@ -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": "Отписаться"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
@ -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>
|
@ -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}
|
||||
|
@ -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
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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 })
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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[]>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
>
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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}
|
@ -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
Loading…
Reference in New Issue
Block a user