UBERF-4361: update inbox ui (#4376)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-01-18 09:44:05 +04:00 committed by GitHub
parent 12924cb79c
commit 13a3914a66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
88 changed files with 1892 additions and 1377 deletions

View File

@ -290,7 +290,7 @@ export function createModel (builder: Builder): void {
{
objectClass: activity.class.Reaction,
action: 'create',
component: activity.component.ReactionAddedMessage,
component: activity.component.ReactionPresenter,
label: activity.string.Reacted,
onlyWithParent: true,
hideIfRemoved: true
@ -317,6 +317,14 @@ export function createModel (builder: Builder): void {
fields: ['createdBy', 'repliedPersons']
})
builder.mixin(activity.class.ActivityMessage, core.class.Class, view.mixin.ObjectPanel, {
component: view.component.AttachedDocPanel
})
builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.NotificationContextPresenter, {
labelPresenter: activity.component.ActivityMessageNotificationLabel
})
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
@ -333,6 +341,14 @@ export function createModel (builder: Builder): void {
},
activity.ids.AddReactionNotification
)
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
messageMatch: {
_class: activity.class.DocUpdateMessage,
objectClass: activity.class.Reaction
},
presenter: activity.component.ReactionNotificationPresenter
})
}
export default activity

View File

@ -25,7 +25,6 @@ export default mergeIds(activityId, activity, {
Attributes: '' as IntlString,
Pinned: '' as IntlString,
Emoji: '' as IntlString,
Reacted: '' as IntlString,
Replies: '' as IntlString
},
filter: {

View File

@ -235,6 +235,14 @@ export function createModel (builder: Builder, options = { addApplication: true
)
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectIcon, {
component: chunter.component.DirectIcon
})
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectIcon, {
component: chunter.component.ChannelIcon
})
spaceClasses.forEach((spaceClass) => {
builder.mixin(spaceClass, core.class.Class, activity.mixin.ActivityDoc, {})
@ -253,7 +261,7 @@ export function createModel (builder: Builder, options = { addApplication: true
})
builder.mixin(spaceClass, core.class.Class, view.mixin.ObjectPanel, {
component: chunter.component.EditChannel
component: chunter.component.ChannelPanel
})
})
@ -262,7 +270,11 @@ export function createModel (builder: Builder, options = { addApplication: true
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: chunter.function.DirectMessageTitleProvider
titleProvider: chunter.function.DirectTitleProvider
})
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: chunter.function.ChannelTitleProvider
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.ClassCollaborators, {
@ -273,14 +285,6 @@ export function createModel (builder: Builder, options = { addApplication: true
fields: ['members']
})
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectPanel, {
component: chunter.component.ChannelViewPanel
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectPanel, {
component: chunter.component.ChannelViewPanel
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectPresenter, {
presenter: chunter.component.DmPresenter
})
@ -309,6 +313,17 @@ export function createModel (builder: Builder, options = { addApplication: true
presenter: chunter.component.ChannelPresenter
})
builder.mixin(chunter.class.ChatMessage, core.class.Class, notification.mixin.NotificationContextPresenter, {
labelPresenter: chunter.component.ChatMessageNotificationLabel
})
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
messageMatch: {
_class: chunter.class.ThreadMessage
},
presenter: chunter.component.ThreadNotificationPresenter
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.SpaceHeader, {
header: chunter.component.DmHeader
})
@ -391,8 +406,7 @@ export function createModel (builder: Builder, options = { addApplication: true
alias: chunterId,
hidden: false,
component: chunter.component.Chat,
aside: chunter.component.ThreadView,
shouldNotify: chunter.function.ShouldNotify
aside: chunter.component.ThreadView
},
chunter.app.Chunter
)

View File

@ -17,7 +17,7 @@ import type { ActivityMessage, DocUpdateMessageViewlet, TxViewlet } from '@hceng
import { chunterId, type Channel } from '@hcengineering/chunter'
import chunter from '@hcengineering/chunter-resources/src/plugin'
import { type Client, type Doc, type Ref } from '@hcengineering/core'
import { type DocNotifyContext, type NotificationGroup } from '@hcengineering/notification'
import { type NotificationGroup } from '@hcengineering/notification'
import type { IntlString, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import type { AnyComponent, Location } from '@hcengineering/ui/src/types'
@ -32,7 +32,9 @@ export default mergeIds(chunterId, chunter, {
BacklinkContent: '' as AnyComponent,
BacklinkReference: '' as AnyComponent,
ChannelsPanel: '' as AnyComponent,
Chat: '' as AnyComponent
Chat: '' as AnyComponent,
ChatMessageNotificationLabel: '' as AnyComponent,
ThreadNotificationPresenter: '' as AnyComponent
},
action: {
MarkCommentUnread: '' as Ref<Action>,
@ -40,8 +42,7 @@ export default mergeIds(chunterId, chunter, {
ArchiveChannel: '' as Ref<Action>,
UnarchiveChannel: '' as Ref<Action>,
ConvertToPrivate: '' as Ref<Action>,
CopyChatMessageLink: '' as Ref<Action<Doc, any>>,
OpenChannel: '' as Ref<Action>
CopyChatMessageLink: '' as Ref<Action<Doc, any>>
},
actionImpl: {
ArchiveChannel: '' as ViewAction,
@ -104,7 +105,6 @@ export default mergeIds(chunterId, chunter, {
function: {
GetLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
ShouldNotify: '' as Resource<(docNotifyContexts: DocNotifyContext[]) => Promise<boolean>>,
DmIdentifierProvider: '' as Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>>,
CanDeleteMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
GetChunterSpaceLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>

View File

@ -245,6 +245,10 @@ export function createModel (builder: Builder): void {
builder.mixin(contact.class.Channel, core.class.Class, activity.mixin.ActivityDoc, {})
builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectIcon, {
component: contact.component.PersonIcon
})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: contact.class.Contact,
components: { input: chunter.component.ChatMessageInput }

View File

@ -23,6 +23,7 @@ import {
type Collection,
type Data,
type Doc,
type DocumentQuery,
type Domain,
DOMAIN_MODEL,
Hierarchy,
@ -68,13 +69,16 @@ import {
notificationId,
type NotificationObjectPresenter,
type ActivityInboxNotification,
type CommonInboxNotification
type CommonInboxNotification,
type NotificationContextPresenter,
type ActivityNotificationViewlet
} from '@hcengineering/notification'
import { type Asset, type IntlString } from '@hcengineering/platform'
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'
@ -159,6 +163,11 @@ export class TNotificationPreview extends TClass implements NotificationPreview
presenter!: AnyComponent
}
@Mixin(notification.mixin.NotificationContextPresenter, core.class.Class)
export class TNotificationContextPresenter extends TClass implements NotificationContextPresenter {
labelPresenter?: AnyComponent
}
@Model(notification.class.DocUpdates, core.class.Doc, DOMAIN_NOTIFICATION)
export class TDocUpdates extends TDoc implements DocUpdates {
@Index(IndexKind.Indexed)
@ -247,6 +256,13 @@ export class TCommonInboxNotification extends TInboxNotification implements Comm
iconProps!: Record<string, any>
}
@Model(notification.class.ActivityNotificationViewlet, core.class.Doc, DOMAIN_MODEL)
export class TActivityNotificationViewlet extends TDoc implements ActivityNotificationViewlet {
messageMatch!: DocumentQuery<Doc>
presenter!: AnyComponent
}
export function createModel (builder: Builder): void {
builder.createModel(
TNotification,
@ -263,7 +279,9 @@ export function createModel (builder: Builder): void {
TDocNotifyContext,
TInboxNotification,
TActivityInboxNotification,
TCommonInboxNotification
TCommonInboxNotification,
TNotificationContextPresenter,
TActivityNotificationViewlet
)
// Temporarily disabled, we should think about it
@ -315,36 +333,11 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
label: notification.string.Inbox,
icon: notification.icon.Notifications,
icon: notification.icon.Inbox,
alias: inboxId,
hidden: true,
locationResolver: notification.resolver.Location,
navigatorModel: {
aside: notification.component.InboxAside,
spaces: [],
specials: [
{
id: 'all',
component: notification.component.Inbox,
icon: activity.icon.Activity,
label: activity.string.AllActivity,
componentProps: {
type: 'all',
label: activity.string.AllActivity
}
},
{
id: 'reactions',
component: notification.component.Inbox,
icon: activity.icon.Emoji,
label: activity.string.Reactions,
componentProps: {
_class: activity.class.Reaction,
label: activity.string.Reactions
}
}
]
}
component: notification.component.Inbox
},
notification.app.Inbox
)
@ -487,17 +480,61 @@ export function createModel (builder: Builder): void {
createAction(
builder,
{
action: notification.actionImpl.HideDocNotifyContext,
label: view.string.Archive,
action: notification.actionImpl.ReadNotifyContext,
label: notification.string.MarkAsRead,
icon: notification.icon.Notifications,
input: 'focus',
visibilityTester: notification.function.CanReadNotifyContext,
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.ReadNotifyContext
)
createAction(
builder,
{
action: notification.actionImpl.UnReadNotifyContext,
label: notification.string.MarkAsUnread,
icon: notification.icon.Notifications,
input: 'focus',
visibilityTester: notification.function.CanUnReadNotifyContext,
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.UnReadNotifyContext
)
createAction(
builder,
{
action: notification.actionImpl.DeleteContextNotifications,
label: notification.string.Archive,
icon: view.icon.Archive,
input: 'focus',
category: view.category.General,
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.DeleteContextNotifications
)
createAction(
builder,
{
action: notification.actionImpl.HideDocNotifyContext,
label: notification.string.DontTrack,
icon: notification.icon.DontTrack,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
context: {
mode: ['browser', 'context'],
group: 'remove'
},
visibilityTester: notification.function.IsDocNotifyContextVisible
visibilityTester: notification.function.IsDocNotifyContextTracked
},
notification.action.HideDocNotifyContext
)
@ -561,6 +598,8 @@ export function createModel (builder: Builder): void {
builder.mixin(notification.class.CommonInboxNotification, core.class.Class, view.mixin.ObjectPresenter, {
presenter: notification.component.CommonInboxNotificationPresenter
})
defineViewlets(builder)
}
export function generateClassNotificationTypes (

View File

@ -48,7 +48,6 @@ export default mergeIds(notificationId, notification, {
},
component: {
NotificationSettings: '' as AnyComponent,
InboxAside: '' as AnyComponent,
ActivityInboxNotificationPresenter: '' as AnyComponent,
CommonInboxNotificationPresenter: '' as AnyComponent
},
@ -56,7 +55,9 @@ export default mergeIds(notificationId, notification, {
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>>
HasDocNotifyContextUnpinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanReadNotifyContext: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanUnReadNotifyContext: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
},
category: {
Notification: '' as Ref<ActionCategory>
@ -73,6 +74,9 @@ export default mergeIds(notificationId, notification, {
UnpinDocNotifyContext: '' as ViewAction,
PinDocNotifyContext: '' as ViewAction,
HideDocNotifyContext: '' as ViewAction,
UnHideDocNotifyContext: '' as ViewAction
UnHideDocNotifyContext: '' as ViewAction,
UnReadNotifyContext: '' as ViewAction,
ReadNotifyContext: '' as ViewAction,
DeleteContextNotifications: '' as ViewAction
}
})

View File

@ -0,0 +1,64 @@
//
// 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.GroupedList,
config: []
},
notification.viewlet.InboxGroupedList
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: notification.class.DocNotifyContext,
descriptor: notification.viewlet.FlatList,
config: []
},
notification.viewlet.InboxFlatList
)
}

View File

@ -162,7 +162,7 @@ function defineFilters (builder: Builder): void {
})
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectIdentifier, {
provider: tracker.function.IssueTitleProvider
provider: tracker.function.IssueIdentifierProvider
})
builder.mixin(tracker.class.Issue, core.class.Class, chunter.mixin.ObjectChatPanel, {

View File

@ -85,7 +85,8 @@ import {
type Viewlet,
type ViewletDescriptor,
type ViewletPreference,
type ObjectIdentifier
type ObjectIdentifier,
type ObjectIcon
} from '@hcengineering/view'
import view from './plugin'
@ -293,6 +294,11 @@ export class TAggregation extends TClass implements Aggregation {
createAggregationManager!: CreateAggregationManagerFunc
}
@Mixin(view.mixin.ObjectIcon, core.class.Class)
export class TObjectIcon extends TClass implements ObjectIcon {
component!: AnyComponent
}
@Model(view.class.ViewletPreference, preference.class.Preference)
export class TViewletPreference extends TPreference implements ViewletPreference {
declare attachedTo: Ref<Viewlet>
@ -444,7 +450,8 @@ export function createModel (builder: Builder): void {
TAllValuesFunc,
TAggregation,
TGroupping,
TObjectIdentifier
TObjectIdentifier,
TObjectIcon
)
classPresenter(

View File

@ -1,9 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="activity" viewBox="0 0 16 16">
<path d="M7.2,6.8C7.1,6.7,6.9,6.6,6.8,6.7c-0.2,0-0.3,0.1-0.4,0.2l-2,2.6c-0.2,0.3-0.2,0.6,0.1,0.8c0.1,0.1,0.2,0.1,0.4,0.1 c0.2,0,0.4-0.1,0.5-0.2l1.6-2.1l1.8,1.4C8.9,9.6,9,9.7,9.2,9.6c0.2,0,0.3-0.1,0.4-0.2l2-2.5c0.2-0.3,0.2-0.6-0.1-0.8 c-0.3-0.2-0.6-0.2-0.8,0.1L9,8.2L7.2,6.8z"/>
<path d="M13.3,4.7c1,0,1.9-0.8,1.9-1.9c0-1-0.8-1.9-1.9-1.9s-1.9,0.8-1.9,1.9C11.4,3.8,12.3,4.7,13.3,4.7z M13.3,2.1 c0.4,0,0.7,0.3,0.7,0.7c0,0.4-0.3,0.7-0.7,0.7s-0.7-0.3-0.7-0.7C12.6,2.4,13,2.1,13.3,2.1z"/>
<path d="M14.1,5.6c-0.3,0-0.6,0.3-0.6,0.6v4.7c0,1.7-1,2.8-2.7,2.8H5.1c-1.6,0-2.7-1.1-2.7-2.8V5.5c0-1.7,1-2.8,2.7-2.8h4.8 c0.3,0,0.6-0.3,0.6-0.6s-0.3-0.6-0.6-0.6H5.1c-2.3,0-3.9,1.6-3.9,4v5.4c0,2.4,1.5,4,3.9,4h5.7c2.3,0,3.9-1.6,3.9-4V6.2 C14.7,5.9,14.4,5.6,14.1,5.6z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 2C12.4477 2 12 2.44772 12 3C12 3.55228 12.4477 4 13 4C13.5523 4 14 3.55228 14 3C14 2.44772 13.5523 2 13 2ZM11 3C11 1.89543 11.8954 1 13 1C14.1046 1 15 1.89543 15 3C15 4.10457 14.1046 5 13 5C11.8954 5 11 4.10457 11 3ZM2 4C2 2.89543 2.89543 2 4 2H9.5C9.77614 2 10 2.22386 10 2.5C10 2.77614 9.77614 3 9.5 3H4C3.44772 3 3 3.44772 3 4V12C3 12.5523 3.44772 13 4 13H12C12.5523 13 13 12.5523 13 12V6.5C13 6.22386 13.2239 6 13.5 6C13.7761 6 14 6.22386 14 6.5V12C14 13.1046 13.1046 14 12 14H4C2.89543 14 2 13.1046 2 12V4ZM6.64645 6.64645C6.84171 6.45118 7.15829 6.45118 7.35355 6.64645L9 8.29289L10.1464 7.14645C10.3417 6.95118 10.6583 6.95118 10.8536 7.14645C11.0488 7.34171 11.0488 7.65829 10.8536 7.85355L9.35355 9.35355C9.15829 9.54882 8.84171 9.54882 8.64645 9.35355L7 7.70711L5.85355 8.85355C5.65829 9.04882 5.34171 9.04882 5.14645 8.85355C4.95118 8.65829 4.95118 8.34171 5.14645 8.14645L6.64645 6.64645Z"/>
</symbol>
<symbol id="emoji" viewBox="0 0 24 24">
<path
d="M10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2ZM10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3ZM7.15467 12.4273C8.66416 13.9463 11.0877 14.0045 12.6671 12.5961L12.8453 12.4273C13.04 12.2314 13.3566 12.2304 13.5524 12.4251C13.7265 12.5981 13.7467 12.8674 13.6123 13.0627L13.5547 13.1322L13.5323 13.1545C11.5691 15.1054 8.39616 15.0953 6.44533 13.1322C6.25069 12.9363 6.25169 12.6197 6.44757 12.4251C6.64344 12.2304 6.96002 12.2314 7.15467 12.4273ZM12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5ZM7.5 7.5C8.05228 7.5 8.5 7.94772 8.5 8.5C8.5 9.05228 8.05228 9.5 7.5 9.5C6.94772 9.5 6.5 9.05228 6.5 8.5C6.5 7.94772 6.94772 7.5 7.5 7.5Z"

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -31,6 +31,7 @@
"Unset": "Unset",
"Update": "Update",
"Updated": "Updated",
"UpdatedCollection": "Updated"
"UpdatedCollection": "Updated",
"Message": "Message"
}
}

View File

@ -31,6 +31,7 @@
"Unset": "Cбросил",
"Update": "Обновить",
"Updated": "Обновил(а)",
"UpdatedCollection": "Обновленные"
"UpdatedCollection": "Обновленные",
"Message": "Сообщение"
}
}

View File

@ -27,7 +27,7 @@
{#if preposition}
<span class="text-sm lower"><Label label={preposition} /></span>
{/if}
<span class="text-sm">
<span class="text-sm" {title}>
<DocNavLink {object} component={panelComponent} shrink={0}>
<span class="overflow-label select-text">{title}</span>
</DocNavLink>

View File

@ -45,9 +45,9 @@
let displayPersons: Person[] = []
$: docNotifyContextByDocStore = inboxClient?.docNotifyContextByDoc
$: inboxNotificationsByContextStore = inboxClient?.inboxNotificationsByContext
$: notificationsByContextStore = inboxClient?.inboxNotificationsByContext
$: hasNew = hasNewReplies(message, $docNotifyContextByDocStore, $inboxNotificationsByContextStore)
$: hasNew = hasNewReplies(message, $docNotifyContextByDocStore, $notificationsByContextStore)
$: updateQuery(persons, $personByIdStore)
function hasNewReplies (

View File

@ -22,12 +22,12 @@
personAccountByIdStore,
personByIdStore
} from '@hcengineering/contact-resources'
import ActivityMessageTemplate from './ActivityMessageTemplate.svelte'
import { Action } from '@hcengineering/ui'
import { Ref } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import { MessageViewer } from '@hcengineering/presentation'
import ActivityMessageTemplate from './ActivityMessageTemplate.svelte'
import ActivityMessageHeader from './ActivityMessageHeader.svelte'
export let value: ActivityInfoMessage
@ -37,6 +37,8 @@
export let shouldScroll: boolean = false
export let embedded: boolean = false
export let withActions: boolean = true
export let excludedActions: string[] = []
export let actions: Action[] = []
export let onClick: (() => void) | undefined = undefined
$: personAccount = $personAccountByIdStore.get((value.createdBy ?? value.modifiedBy) as Ref<PersonAccount>)
@ -66,6 +68,8 @@
{shouldScroll}
{embedded}
{withActions}
{actions}
{excludedActions}
viewlet={undefined}
{onClick}
>

View File

@ -0,0 +1,118 @@
<!--
// 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 { Label } from '@hcengineering/ui'
import { ActivityInboxNotification, DocNotifyContext } from '@hcengineering/notification'
import activity, { ActivityMessage, DocUpdateMessage, Reaction } 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 ActivityDocLink from '../ActivityDocLink.svelte'
import ReactionPresenter from '../reactions/ReactionPresenter.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>)
</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}
<style lang="scss">
.label {
width: 20rem;
max-width: 20rem;
}
.labels {
display: flex;
flex-direction: column;
justify-content: space-between;
}
</style>

View File

@ -30,7 +30,7 @@
export let hideReplies = false
export let skipLabel = false
export let actions: Action[] = []
export let excludedActions: string[] = []
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
@ -55,6 +55,7 @@
showEmbedded,
hideReplies,
actions,
excludedActions,
onClick,
onReply
}}

View File

@ -34,9 +34,9 @@
import SaveMessageAction from '../SaveMessageAction.svelte'
export let message: DisplayActivityMessage
export let parentMessage: DisplayActivityMessage | undefined
export let parentMessage: DisplayActivityMessage | undefined = undefined
export let viewlet: ActivityMessageViewlet | undefined
export let viewlet: ActivityMessageViewlet | undefined = undefined
export let person: Person | undefined = undefined
export let actions: Action[] = []
export let excludedActions: string[] = []
@ -211,6 +211,7 @@
overflow: hidden;
border: 1px solid transparent;
border-radius: 0.25rem;
width: calc(100% - 2rem);
&.clickable {
cursor: pointer;
@ -225,7 +226,6 @@
}
&.embedded {
background: var(--theme-navpanel-icons-divider);
padding: 0;
.content {
@ -288,6 +288,7 @@
.embeddedMarker {
width: 6px;
background: var(--theme-link-color);
border-radius: 0.5rem;
background: var(--secondary-button-default);
}
</style>

View File

@ -46,6 +46,7 @@
export let showEmbedded = false
export let hideReplies = false
export let actions: Action[] = []
export let excludedActions: string[] = []
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
@ -150,6 +151,7 @@
{isSelected}
{shouldScroll}
{embedded}
{excludedActions}
{withActions}
{viewlet}
{showEmbedded}

View File

@ -0,0 +1,49 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ActivityInboxNotification } from '@hcengineering/notification'
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import { createQuery } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import { 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 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.fragment = notification.docNotifyContext
loc.query = { message: notification.attachedTo }
navigate(loc)
}
</script>
{#if embedded && parentMessage}
<ActivityMessagePresenter value={parentMessage} skipLabel embedded onReply={handleReply} {onClick} />
{:else if !embedded && message}
<ActivityMessagePresenter value={message} skipLabel showEmbedded onReply={handleReply} {onClick} />
{/if}

View File

@ -19,7 +19,9 @@ 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 ReactionAddedMessage from './components/reactions/ReactionAddedMessage.svelte'
import ReactionPresenter from './components/reactions/ReactionPresenter.svelte'
import ReactionNotificationPresenter from './components/reactions/ReactionNotificationPresenter.svelte'
import ActivityMessageNotificationLabel from './components/activity-message/ActivityMessageNotificationLabel.svelte'
import { getMessageFragment, attributesFilter, pinnedFilter, allFilter } from './activityMessagesUtils'
@ -32,16 +34,19 @@ export { default as ActivityMessageTemplate } from './components/activity-messag
export { default as ActivityMessagePresenter } from './components/activity-message/ActivityMessagePresenter.svelte'
export { default as ActivityExtension } from './components/ActivityExtension.svelte'
export { default as ActivityScrolledView } from './components/ActivityScrolledView.svelte'
export { default as ActivityMessageHeader } from './components/activity-message/ActivityMessageHeader.svelte'
export { default as ActivityDocLink } from './components/ActivityDocLink.svelte'
export { default as ReactionPresenter } from './components/reactions/ReactionPresenter.svelte'
export { default as ActivityMessageNotificationLabel } from './components/activity-message/ActivityMessageNotificationLabel.svelte'
export default async (): Promise<Resources> => ({
component: {
Activity,
ActivityMessagePresenter,
DocUpdateMessagePresenter,
ReactionAddedMessage,
ActivityInfoMessagePresenter
ReactionPresenter,
ActivityInfoMessagePresenter,
ReactionNotificationPresenter,
ActivityMessageNotificationLabel
},
filter: {
AttributesFilter: attributesFilter,

View File

@ -333,14 +333,18 @@ export default plugin(activityId, {
AllActivity: '' as IntlString,
Reactions: '' as IntlString,
LastReply: '' as IntlString,
RepliesCount: '' as IntlString
RepliesCount: '' as IntlString,
Reacted: '' as IntlString,
Message: '' as IntlString
},
component: {
Activity: '' as AnyComponent,
ActivityMessagePresenter: '' as AnyComponent,
DocUpdateMessagePresenter: '' as AnyComponent,
ActivityInfoMessagePresenter: '' as AnyComponent,
ReactionAddedMessage: '' as AnyComponent
ReactionPresenter: '' as AnyComponent,
ReactionNotificationPresenter: '' as AnyComponent,
ActivityMessageNotificationLabel: '' as AnyComponent
},
ids: {
AllFilter: '' as Ref<ActivityMessagesFilter>

View File

@ -83,7 +83,8 @@
"NewestFirst": "Newest first",
"ReplyToThread": "Reply to thread",
"SentMessage": "Sent message",
"Direct": "direct",
"RepliedToThread": "Replied to thread"
"Direct": "Direct",
"RepliedToThread": "Replied to thread",
"RepliedTo": "replied to: {message}"
}
}

View File

@ -83,7 +83,8 @@
"NewestFirst": "Сначала новые",
"ReplyToThread": "Ответить в канале",
"SentMessage": "Отправил(а) сообщение",
"Direct": "личные сообщения",
"RepliedToThread": "Ответил(а) в канале"
"Direct": "Личные сообщения",
"RepliedToThread": "Ответил(а) в канале",
"RepliedTo": "Ответил(а) на: {message}"
}
}

View File

@ -19,22 +19,26 @@
import { onDestroy } from 'svelte'
import activity, { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity'
import { ActivityScrolledView } from '@hcengineering/activity-resources'
import { getClient } from '@hcengineering/presentation'
import chunter from '../plugin'
export let notifyContext: DocNotifyContext
export let context: DocNotifyContext
export let object: Doc
export let filterId: Ref<ActivityMessagesFilter> = activity.ids.AllFilter
const client = getClient()
const hierarchy = client.getHierarchy()
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
const unsubscribe = locationStore.subscribe((newLocation) => {
selectedMessageId = newLocation.fragment as Ref<ActivityMessage>
selectedMessageId = newLocation.query?.message as Ref<ActivityMessage> | undefined
})
onDestroy(unsubscribe)
$: isDocChannel = ![chunter.class.DirectMessage, chunter.class.Channel].includes(notifyContext.attachedToClass)
$: isDocChannel = !hierarchy.isDerived(context.attachedToClass, chunter.class.ChunterSpace)
$: messagesClass = isDocChannel ? activity.class.ActivityMessage : chunter.class.ChatMessage
$: collection = isDocChannel ? 'comments' : 'messages'
</script>
@ -47,5 +51,5 @@
startFromBottom
{selectedMessageId}
{collection}
lastViewedTimestamp={notifyContext.lastViewedTimestamp}
lastViewedTimestamp={context.lastViewedTimestamp}
/>

View File

@ -23,6 +23,7 @@
import { getChannelIcon } from '../utils'
export let object: Doc
export let allowClose = false
const client = getClient()
const hierarchy = client.getHierarchy()
@ -45,6 +46,14 @@
label={title}
intlLabel={title ? undefined : chunter.string.Channel}
description={topic}
{allowClose}
on:close
/>
{/await}
</div>
<style lang="scss">
.ac-header {
padding: 0.5rem 1rem;
}
</style>

View File

@ -0,0 +1,28 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Icon, IconSize } from '@hcengineering/ui'
import chunter, { Channel } from '@hcengineering/chunter'
import Lock from './icons/Lock.svelte'
export let value: Channel
export let size: IconSize = 'small'
</script>
{#if value.private}
<Lock {size} />
{:else}
<Icon icon={chunter.icon.Hashtag} {size} />
{/if}

View File

@ -0,0 +1,51 @@
<!--
// 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 { Class, Ref } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { ChunterSpace } from '@hcengineering/chunter'
import ChannelPresenter from './ChannelView.svelte'
import ThreadViewPanel from './threads/ThreadViewPanel.svelte'
export let _id: Ref<ChunterSpace>
export let _class: Ref<Class<ChunterSpace>>
export let context: DocNotifyContext
const objectQuery = createQuery()
const client = getClient()
const hierarchy = client.getHierarchy()
let object: ChunterSpace | undefined = undefined
let threadId: Ref<ActivityMessage> | undefined = undefined
$: threadId = hierarchy.isDerived(context.attachedToClass, activity.class.ActivityMessage)
? (context.attachedTo as Ref<ActivityMessage>)
: undefined
$: objectQuery.query(_class, { _id }, (res) => {
object = res[0]
})
</script>
{#if threadId}
<ThreadViewPanel _id={threadId} on:close />
{:else if object}
<div class="antiComponent">
<ChannelPresenter {object} {context} allowClose on:close />
</div>
{/if}

View File

@ -16,16 +16,17 @@
import { Ref, Doc } from '@hcengineering/core'
import { getLocation, navigate } from '@hcengineering/ui'
import { DocNotifyContext } from '@hcengineering/notification'
import activity, { ActivityMessagesFilter } from '@hcengineering/activity'
import activity, { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity'
import { ChatMessage } from '@hcengineering/chunter'
import Channel from './Channel.svelte'
import PinnedMessages from './PinnedMessages.svelte'
import ChannelHeader from './ChannelHeader.svelte'
export let notifyContext: DocNotifyContext
export let context: DocNotifyContext
export let object: Doc
export let filterId: Ref<ActivityMessagesFilter> = activity.ids.AllFilter
export let allowClose = false
function openThread (_id: Ref<ChatMessage>) {
const loc = getLocation()
@ -34,10 +35,10 @@
}
</script>
<ChannelHeader {object} />
<PinnedMessages {notifyContext} />
<ChannelHeader {object} {allowClose} on:close />
<PinnedMessages {context} />
<Channel
{notifyContext}
{context}
{object}
{filterId}
on:openThread={(e) => {

View File

@ -1,44 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Doc, Ref } from '@hcengineering/core'
import notification, { DocNotifyContext } from '@hcengineering/notification'
import { createQuery } from '@hcengineering/presentation'
import ChannelPresenter from './ChannelView.svelte'
export let _id: Ref<DocNotifyContext>
const objectQuery = createQuery()
const contextQuery = createQuery()
let notifyContext: DocNotifyContext | undefined = undefined
let object: Doc | undefined = undefined
$: contextQuery.query(notification.class.DocNotifyContext, { _id }, (res) => {
notifyContext = res[0]
})
$: notifyContext &&
objectQuery.query(notifyContext.attachedToClass, { _id: notifyContext.attachedTo }, (res) => {
object = res[0]
})
</script>
{#if notifyContext && object}
<div class="antiComponent">
<ChannelPresenter {notifyContext} {object} />
</div>
{/if}

View File

@ -0,0 +1,95 @@
<!--
// 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 { DirectMessage } from '@hcengineering/chunter'
import { Avatar, CombineAvatars } from '@hcengineering/contact-resources'
import { Icon, IconSize } from '@hcengineering/ui'
import contact, { Person } from '@hcengineering/contact'
import { classIcon } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import chunter from '../plugin'
import { getDmPersons } from '../utils'
export let value: DirectMessage
export let size: IconSize = 'small'
const visiblePersons = 4
const client = getClient()
let persons: Person[] = []
$: getDmPersons(client, value).then((res) => {
persons = res
})
</script>
{#if persons.length === 0}
<Icon icon={classIcon(client, value._class) ?? chunter.icon.Chunter} {size} />
{/if}
{#if persons.length === 1}
<Avatar avatar={persons[0].avatar} {size} />
{/if}
{#if persons.length > 1 && size === 'medium'}
<div class="group">
{#each persons.slice(0, visiblePersons - 1) as person}
<Avatar avatar={person.avatar} size="tiny" />
{/each}
{#if persons.length > visiblePersons}
<div class="rect">
+{persons.length - visiblePersons + 1}
</div>
{/if}
{#if persons.length < visiblePersons}
{#each Array(visiblePersons - persons.length) as _}
<div class="rect" />
{/each}
{/if}
</div>
{/if}
{#if persons.length > 1 && size !== 'medium'}
<CombineAvatars _class={contact.class.Person} items={persons.map(({ _id }) => _id)} {size} limit={visiblePersons} />
{/if}
<style lang="scss">
.group {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
height: 2.5rem;
width: 2.5rem;
min-width: 2.5rem;
min-height: 2.5rem;
border: 1px solid transparent;
border-radius: 0.5rem;
}
.rect {
display: flex;
align-items: center;
justify-content: center;
width: 1.13rem;
height: 1.13rem;
border: 1px solid transparent;
border-radius: 0.25rem;
background-color: var(--theme-button-hovered);
font-size: 0.688rem;
font-weight: 500;
}
</style>

View File

@ -18,7 +18,7 @@
import { NavLink } from '@hcengineering/view-resources'
import { getDmName } from '../utils'
import DmIconPresenter from './DmIconPresenter.svelte'
import DirectIcon from './DirectIcon.svelte'
export let value: DirectMessage
export let disabled = false
@ -30,7 +30,7 @@
{#await getDmName(client, value) then name}
<NavLink app={chunterId} space={value._id} {disabled}>
<div class="flex-presenter">
<DmIconPresenter {value} />
<DirectIcon {value} />
<span class="label">{name}</span>
</div>
</NavLink>

View File

@ -14,7 +14,9 @@
-->
<script lang="ts">
import type { Asset, IntlString } from '@hcengineering/platform'
import { AnySvelteComponent, Icon, Label, SearchEdit } from '@hcengineering/ui'
import { AnySvelteComponent, Button, Icon, IconClose, Label, SearchEdit } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { userSearch } from '../index'
import { navigateToSpecial } from '../utils'
@ -23,15 +25,30 @@
export let label: string | undefined = undefined
export let intlLabel: IntlString | undefined = undefined
export let description: string | undefined = undefined
export let allowClose = false
const dispatch = createEventDispatcher()
let userSearch_: string
userSearch.subscribe((v) => (userSearch_ = v))
</script>
<div class="ac-header__wrap-description">
<div class="ac-header__wrap-description header">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="ac-header__wrap-title" on:click>
{#if allowClose}
<Button
focusIndex={10001}
icon={IconClose}
iconProps={{ size: 'medium' }}
kind={'icon'}
on:click={() => {
dispatch('close')
}}
/>
<div class="antiHSpacer x2" />
{/if}
{#if icon}<div class="ac-header__icon"><Icon {icon} size={'small'} {iconProps} /></div>{/if}
{#if label}
<span class="ac-header__title">{label}</span>

View File

@ -21,7 +21,7 @@
import chunter from '../plugin'
export let notifyContext: DocNotifyContext
export let context: DocNotifyContext
const pinnedQuery = createQuery()
@ -29,7 +29,7 @@
$: pinnedQuery.query(
activity.class.ActivityMessage,
{ attachedTo: notifyContext.attachedTo, isPinned: true },
{ attachedTo: context.attachedTo, isPinned: true },
(res: ActivityMessage[]) => {
pinnedMessagesCount = res.length
}
@ -37,7 +37,7 @@
function openMessagesPopup (ev: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) {
showPopup(
PinnedMessagesPopup,
{ attachedTo: notifyContext.attachedTo, attachedToClass: notifyContext.attachedToClass },
{ attachedTo: context.attachedTo, attachedToClass: context.attachedToClass },
eventToHTMLElement(ev)
)
}

View File

@ -39,6 +39,8 @@
export let showEmbedded = false
export let hideReplies = false
export let skipLabel = false
export let actions: Action[] = []
export let excludedActions: string[] = []
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
@ -125,11 +127,11 @@
}
let isEditing = false
let actions: Action[] = []
let additionalActions: Action[] = []
$: isOwn = user !== undefined && user._id === currentAccount._id
$: actions = [
$: additionalActions = [
...(isOwn
? [
{
@ -139,10 +141,10 @@
action: handleEditAction
}
]
: [])
: []),
...actions
]
$: excludedActions = []
let refInput: ChatMessageInput
</script>
@ -159,7 +161,7 @@
{shouldScroll}
{embedded}
{withActions}
{actions}
actions={additionalActions}
{showEmbedded}
{hideReplies}
{onClick}

View File

@ -88,10 +88,6 @@
object = res[0]
})
$: if (selectedContext) {
console.log({ selectedContext: selectedContext.attachedToClass })
}
$: isDocChatOpened =
selectedContext !== undefined &&
![chunter.class.Channel, chunter.class.DirectMessage].includes(selectedContext.attachedToClass)
@ -139,7 +135,7 @@
{/if}
{#if selectedContext && object}
{#key selectedContext._id}
<ChannelView notifyContext={selectedContext} {object} {filterId} />
<ChannelView context={selectedContext} {object} {filterId} />
{/key}
{/if}
</div>

View File

@ -113,6 +113,7 @@
selected={selectedContextId === notifyContext._id}
icon={getChannelIcon(doc)}
iconProps={{ value: doc }}
iconSize="x-small"
showNotify={hasNewMessages(notifyContext, $inboxNotificationsStore, threadMessages)}
actions={async () => await getItemActions(notifyContext)}
indent

View File

@ -1,5 +1,7 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
import { IconSize } from '@hcengineering/ui'
export let size: IconSize
const fill: string = 'currentColor'
</script>

View File

@ -0,0 +1,74 @@
<!--
// 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 { 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 { getDocLinkTitle } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import chunter from '../../plugin'
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 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]
})
$: 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
</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}
<style lang="scss">
.label {
width: 20rem;
max-width: 20rem;
}
</style>

View File

@ -0,0 +1,34 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ThreadMessage } from '@hcengineering/chunter'
import ThreadMessagePresenter from '../threads/ThreadMessagePresenter.svelte'
import { getLocation, navigate } from '@hcengineering/ui'
import { ActivityInboxNotification } from '@hcengineering/notification'
export let message: ThreadMessage
export let notification: ActivityInboxNotification
export let embedded = false
export let onClick: (() => void) | undefined = undefined
function handleReply (): void {
const loc = getLocation()
loc.fragment = notification.docNotifyContext
loc.query = { message: notification.attachedTo }
navigate(loc)
}
</script>
<ThreadMessagePresenter value={message} {embedded} showEmbedded={!embedded} onReply={handleReply} {onClick} />

View File

@ -26,6 +26,7 @@
export let showEmbedded = false
export let skipLabel = false
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
</script>
<ChatMessagePresenter
@ -39,4 +40,5 @@
{embedded}
{skipLabel}
{onClick}
{onReply}
/>

View File

@ -32,7 +32,7 @@
let message: DisplayActivityMessage | undefined = undefined
locationStore.subscribe((newLocation) => {
selectedMessageId = newLocation.fragment as Ref<ActivityMessage>
selectedMessageId = newLocation.query?.message as Ref<ActivityMessage> | undefined
})
$: messageQuery.query(activity.class.ActivityMessage, { _id }, (result: ActivityMessage[]) => {

View File

@ -15,12 +15,19 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { ActivityMessage } from '@hcengineering/activity'
import { location as locationStore } from '@hcengineering/ui'
import ThreadView from './ThreadView.svelte'
export let _id: Ref<ActivityMessage>
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
locationStore.subscribe((newLocation) => {
selectedMessageId = newLocation.query?.message as Ref<ActivityMessage> | undefined
})
</script>
<div class="antiPanel-component">
<ThreadView {_id} />
<ThreadView {_id} {selectedMessageId} on:close />
</div>

View File

@ -24,7 +24,7 @@ import { type DocNotifyContext } from '@hcengineering/notification'
import ChannelPresenter from './components/ChannelPresenter.svelte'
import ChannelView from './components/ChannelView.svelte'
import ChannelViewPanel from './components/ChannelViewPanel.svelte'
import ChannelPanel from './components/ChannelPanel.svelte'
import ChunterBrowser from './components/chat/specials/ChunterBrowser.svelte'
import ConvertDmToPrivateChannelModal from './components/ConvertDmToPrivateChannel.svelte'
import CreateChannel from './components/chat/create/CreateChannel.svelte'
@ -49,10 +49,15 @@ import ThreadParentPresenter from './components/threads/ThreadParentPresenter.sv
import ChannelHeader from './components/ChannelHeader.svelte'
import SavedMessages from './components/chat/specials/SavedMessages.svelte'
import Threads from './components/chat/specials/Threads.svelte'
import DirectIcon from './components/DirectIcon.svelte'
import ChannelIcon from './components/ChannelIcon.svelte'
import ThreadNotificationPresenter from './components/notification/ThreadNotificationPresenter.svelte'
import ChatMessageNotificationLabel from './components/notification/ChatMessageNotificationLabel.svelte'
import { updateBacklinksList } from './backlinks'
import {
DirectMessageTitleProvider,
ChannelTitleProvider,
DirectTitleProvider,
canDeleteMessage,
chunterSpaceLinkFragmentProvider,
dmIdentifierProvider,
@ -148,13 +153,6 @@ export async function chunterBrowserVisible (): Promise<boolean> {
return false
}
export function shouldNotify (docNotifyContexts: DocNotifyContext[]): boolean {
return docNotifyContexts.some(
({ hidden, lastViewedTimestamp, lastUpdateTimestamp }) =>
!hidden && (lastViewedTimestamp ?? 0) < (lastUpdateTimestamp ?? 0)
)
}
async function update (source: Doc, key: string, target: RelatedDocument[], msg: IntlString): Promise<void> {
const message = await translate(msg, {})
const backlinks: Array<Data<Backlink>> = target.map((it) => ({
@ -189,8 +187,6 @@ export async function deleteChatMessage (message: ChatMessage): Promise<void> {
}
export async function replyToThread (message: ActivityMessage): Promise<void> {
console.log('reply', { message })
const loc = getLocation()
loc.path[4] = message._id
navigate(loc)
@ -208,7 +204,7 @@ export default async (): Promise<Resources> => ({
ThreadViewPanel,
ChannelHeader,
ChannelView,
ChannelViewPanel,
ChannelPanel,
ChannelPresenter,
DirectMessagePresenter,
ChannelPreview,
@ -226,15 +222,19 @@ export default async (): Promise<Resources> => ({
ChatMessagesPresenter,
Chat,
ThreadMessagePresenter,
Threads
Threads,
DirectIcon,
ChannelIcon,
ChatMessageNotificationLabel,
ThreadNotificationPresenter
},
function: {
GetDmName: getDmName,
ChunterBrowserVisible: chunterBrowserVisible,
GetFragment: getTitle,
GetLink: getLink,
DirectMessageTitleProvider,
ShouldNotify: shouldNotify,
DirectTitleProvider,
ChannelTitleProvider,
DmIdentifierProvider: dmIdentifierProvider,
CanDeleteMessage: canDeleteMessage,
GetChunterSpaceLinkFragment: chunterSpaceLinkFragmentProvider

View File

@ -25,7 +25,7 @@ export default mergeIds(chunterId, chunter, {
CreateChannel: '' as AnyComponent,
CreateDirectMessage: '' as AnyComponent,
ChannelHeader: '' as AnyComponent,
ChannelViewPanel: '' as AnyComponent,
ChannelPanel: '' as AnyComponent,
ThreadViewPanel: '' as AnyComponent,
ThreadParentPresenter: '' as AnyComponent,
EditChannel: '' as AnyComponent,
@ -35,11 +35,14 @@ export default mergeIds(chunterId, chunter, {
CreateDocChannel: '' as AnyComponent,
SavedMessages: '' as AnyComponent,
Threads: '' as AnyComponent,
ChunterBrowser: '' as AnyComponent
ChunterBrowser: '' as AnyComponent,
DirectIcon: '' as AnyComponent,
ChannelIcon: '' as AnyComponent
},
function: {
GetDmName: '' as Resource<(client: Client, space: Space) => Promise<string>>,
DirectMessageTitleProvider: '' as Resource<(client: Client, id: Ref<Doc>) => Promise<string>>,
DirectTitleProvider: '' as Resource<(client: Client, id: Ref<Doc>) => Promise<string>>,
ChannelTitleProvider: '' as Resource<(client: Client, id: Ref<Doc>) => Promise<string>>,
ChunterBrowserVisible: '' as Resource<(spaces: Space[]) => Promise<boolean>>
},
actionImpl: {
@ -53,7 +56,6 @@ export default mergeIds(chunterId, chunter, {
string: {
Channel: '' as IntlString,
DirectMessage: '' as IntlString,
Channels: '' as IntlString,
DirectMessages: '' as IntlString,
CreateChannel: '' as IntlString,
NewDirectMessage: '' as IntlString,
@ -97,7 +99,6 @@ export default mergeIds(chunterId, chunter, {
NoMessages: '' as IntlString,
On: '' as IntlString,
Mentioned: '' as IntlString,
SentMessage: '' as IntlString,
Direct: '' as IntlString
SentMessage: '' as IntlString
}
})

View File

@ -32,16 +32,16 @@ import {
navigate
} from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import { get, type Unsubscriber } from 'svelte/store'
import chunter from './plugin'
import { type Asset, translate } from '@hcengineering/platform'
import Lock from './components/icons/Lock.svelte'
import { classIcon } from '@hcengineering/view-resources'
import DmIconPresenter from './components/DmIconPresenter.svelte'
import { type ActivityMessage } from '@hcengineering/activity'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { type DocNotifyContext } from '@hcengineering/notification'
import { get, type Unsubscriber } from 'svelte/store'
import chunter from './plugin'
import DirectIcon from './components/DirectIcon.svelte'
import ChannelIcon from './components/ChannelIcon.svelte'
export async function getDmName (client: Client, space?: Space): Promise<string> {
if (space === undefined) {
@ -125,14 +125,24 @@ export async function getDmPersons (client: Client, space: Space): Promise<Perso
return persons
}
export async function DirectMessageTitleProvider (client: Client, id: Ref<DirectMessage>): Promise<string> {
const space = await client.findOne(chunter.class.DirectMessage, { _id: id })
export async function DirectTitleProvider (client: Client, id: Ref<DirectMessage>): Promise<string> {
const direct = await client.findOne(chunter.class.DirectMessage, { _id: id })
if (space === undefined) {
if (direct === undefined) {
return ''
}
return await getDmName(client, space)
return await getDmName(client, direct)
}
export async function ChannelTitleProvider (client: Client, id: Ref<Channel>): Promise<string> {
const channel = await client.findOne(chunter.class.Channel, { _id: id })
if (channel === undefined) {
return ''
}
return channel.name
}
export async function openMessageFromSpecial (message?: ActivityMessage): Promise<void> {
@ -181,7 +191,6 @@ export enum SearchType {
export async function getLink (message: ActivityMessage): Promise<string> {
const inboxClient = InboxNotificationsClientImpl.getClient()
const fragment = message._id
const location = getCurrentResolvedLocation()
let context: DocNotifyContext | undefined
@ -199,7 +208,7 @@ export async function getLink (message: ActivityMessage): Promise<string> {
return ''
}
return `${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${chunterId}/${context._id}${threadParent}#${fragment}`
return `${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${chunterId}/${context._id}${threadParent}?message=${message._id}`
}
export async function getTitle (doc: Doc): Promise<string> {
@ -237,13 +246,11 @@ export function getChannelIcon (doc: Doc): Asset | AnySvelteComponent | undefine
const client = getClient()
if (doc._class === chunter.class.Channel) {
const channel = doc as Channel
return channel.private ? Lock : classIcon(client, channel._class)
return ChannelIcon
}
if (doc._class === chunter.class.DirectMessage) {
return DmIconPresenter
return DirectIcon
}
return classIcon(client, doc._class)

View File

@ -218,7 +218,10 @@ export default plugin(chunterId, {
Docs: '' as IntlString,
Chat: '' as IntlString,
ThreadMessage: '' as IntlString,
ReplyToThread: '' as IntlString
ReplyToThread: '' as IntlString,
Channels: '' as IntlString,
Direct: '' as IntlString,
RepliedTo: '' as IntlString
},
ids: {
DMNotification: '' as Ref<NotificationType>,
@ -236,6 +239,7 @@ export default plugin(chunterId, {
},
action: {
DeleteChatMessage: '' as Ref<Action>,
ReplyToThread: '' as Ref<Action>
ReplyToThread: '' as Ref<Action>,
OpenChannel: '' as Ref<Action>
}
})

View File

@ -48,7 +48,7 @@
export let direct: Blob | undefined = undefined
export let size: IconSize
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let variant: 'circle' | 'roundedRect' = 'circle'
export let variant: 'circle' | 'roundedRect' = 'roundedRect'
let url: string[] | undefined
let avatarProvider: AvatarProvider | undefined
@ -131,7 +131,7 @@
}
&.roundedRect {
border-radius: 6px;
border-radius: 0.5rem;
}
&.no-img {
@ -185,6 +185,10 @@
font-size: 0.625rem;
letter-spacing: -0.05em;
}
&.roundedRect {
border-radius: 0.25rem;
}
}
.ava-card {
@ -246,6 +250,7 @@
font-size: 2rem;
}
}
.ava-x-large {
width: 7.5rem; // 120
height: 7.5rem;
@ -254,7 +259,12 @@
font-weight: 500;
font-size: 3.5rem;
}
&.roundedRect {
border-radius: 1rem;
}
}
.ava-2x-large {
width: 10rem; // 120
height: 10rem;
@ -263,6 +273,10 @@
font-weight: 500;
font-size: 4.75rem;
}
&.roundedRect {
border-radius: 1rem;
}
}
.ava-blur {
@ -279,7 +293,16 @@
}
&.roundedRect {
border-radius: 6px;
border-radius: 0.5rem;
.ava-tiny {
border-radius: 0.25rem;
}
.ava-x-large,
.ava-2x-large {
border-radius: 1rem;
}
}
}

View File

@ -95,9 +95,6 @@
.combine-avatar.x-large:not(:first-child) {
margin-left: calc(1px - (7.5rem / 2));
}
.combine-avatar:not(:last-child) {
mask: radial-gradient(circle at 100% 50%, rgba(0, 0, 0, 0) 48.5%, rgb(0, 0, 0) 50%);
}
.combine-avatar.inline,
.combine-avatar.tiny,
.combine-avatar.card,
@ -124,7 +121,7 @@
height: 100%;
background-color: var(--theme-bg-color);
border: 1px solid var(--theme-divider-color);
border-radius: 50%;
border-radius: 0.25rem;
opacity: 0.9;
z-index: 1;
}

View File

@ -1,5 +1,5 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
@ -13,27 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { DirectMessage } from '@hcengineering/chunter'
import { getClient } from '@hcengineering/presentation'
import { Person } from '@hcengineering/contact'
import { Avatar } from '@hcengineering/contact-resources'
import { IconSize } from '@hcengineering/ui'
import Avatar from './Avatar.svelte'
import { getDmPersons } from '../utils'
export let value: DirectMessage
const client = getClient()
let persons: Person[] = []
$: getDmPersons(client, value).then((res) => {
persons = res
})
export let value: Person
export let size: IconSize = 'small'
</script>
{#each persons as person}
{#if person}
<div class="icon">
<Avatar size="x-small" avatar={person.avatar} name={person.name} />
</div>
{/if}
{/each}
<Avatar avatar={value.avatar} {size} />

View File

@ -17,7 +17,7 @@
import { Asset } from '@hcengineering/platform'
export let size: IconSize
export let variant: 'circle' | 'roundedRect' = 'circle'
export let variant: 'circle' | 'roundedRect' = 'roundedRect'
export let icon: Asset | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined
@ -52,7 +52,7 @@
}
&.roundedRect {
border-radius: 0.375rem;
border-radius: 0.5rem;
}
&.x-small {
@ -101,6 +101,10 @@
.text {
font-size: 3.5rem;
}
&.roundedRect {
border-radius: 1rem;
}
}
}
</style>

View File

@ -98,6 +98,7 @@ import IconMembers from './components/icons/Members.svelte'
import TxNameChange from './components/activity/TxNameChange.svelte'
import NameChangedActivityMessage from './components/activity/NameChangedActivityMessage.svelte'
import SystemAvatar from './components/SystemAvatar.svelte'
import PersonIcon from './components/PersonIcon.svelte'
import contact from './plugin'
import {
@ -317,7 +318,8 @@ export default async (): Promise<Resources> => ({
EmployeeFilterValuePresenter,
PersonAccountFilterValuePresenter,
DeleteConfirmationPopup,
PersonAccountRefPresenter
PersonAccountRefPresenter,
PersonIcon
},
completion: {
EmployeeQuery: async (

View File

@ -195,7 +195,8 @@ export const contactPlugin = plugin(contactId, {
ChannelPresenter: '' as AnyComponent,
SpaceMembers: '' as AnyComponent,
DeleteConfirmationPopup: '' as AnyComponent,
AccountArrayEditor: '' as AnyComponent
AccountArrayEditor: '' as AnyComponent,
PersonIcon: '' as AnyComponent
},
channelProvider: {
Email: '' as Ref<ChannelProvider>,

View File

@ -2,12 +2,8 @@
<symbol id="notifications" viewBox="0 0 16 16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 1.5C8.5 1.22386 8.27614 1 8 1C7.72386 1 7.5 1.22386 7.5 1.5V2.02746C5.25002 2.27619 3.5 4.18372 3.5 6.5V7.44766C3.5 8.01534 3.3068 8.56611 2.95217 9.00939L2.09004 10.0871C1.70809 10.5645 1.5 11.1577 1.5 11.7691C1.5 12.4489 2.05108 13 2.73087 13H6C6 13.2626 6.05173 13.5227 6.15224 13.7654C6.25275 14.008 6.40007 14.2285 6.58579 14.4142C6.7715 14.5999 6.99198 14.7472 7.23463 14.8478C7.47728 14.9483 7.73736 15 8 15C8.26264 15 8.52272 14.9483 8.76537 14.8478C9.00802 14.7472 9.2285 14.5999 9.41421 14.4142C9.59993 14.2285 9.74725 14.008 9.84776 13.7654C9.94827 13.5227 10 13.2626 10 13H13.2691C13.9489 13 14.5 12.4489 14.5 11.7691C14.5 11.1577 14.2919 10.5645 13.91 10.0871L13.0478 9.00939C12.6932 8.56611 12.5 8.01534 12.5 7.44766V6.5C12.5 4.18372 10.75 2.27619 8.5 2.02746V1.5ZM9 13H7C7 13.1313 7.02587 13.2614 7.07612 13.3827C7.12638 13.504 7.20003 13.6142 7.29289 13.7071C7.38575 13.8 7.49599 13.8736 7.61732 13.9239C7.73864 13.9741 7.86868 14 8 14C8.13132 14 8.26136 13.9741 8.38268 13.9239C8.50401 13.8736 8.61425 13.8 8.70711 13.7071C8.79997 13.6142 8.87362 13.504 8.92388 13.3827C8.97413 13.2614 9 13.1313 9 13ZM2.5 11.7691C2.5 11.8966 2.60336 12 2.73087 12H13.2691C13.3966 12 13.5 11.8966 13.5 11.7691C13.5 11.3848 13.3692 11.0119 13.1291 10.7118L12.267 9.63409C11.7705 9.01349 11.5 8.24241 11.5 7.44766V6.5C11.5 4.567 9.933 3 8 3C6.067 3 4.5 4.567 4.5 6.5V7.44766C4.5 8.24241 4.22952 9.01349 3.73304 9.63409L2.87091 10.7118C2.63081 11.0119 2.5 11.3848 2.5 11.7691Z" />
</symbol>
<symbol id="inbox" viewBox="0 0 32 32">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.2 8.39976C9.38885 8.14795 9.68524 7.99976 10 7.99976H22C22.3148 7.99976 22.6111 8.14795 22.8 8.39976L27 13.9998H19C18.4477 13.9998 18 14.4475 18 14.9998C18 15.2624 17.9483 15.5225 17.8478 15.7651C17.7472 16.0078 17.5999 16.2283 17.4142 16.414C17.2285 16.5997 17.008 16.747 16.7654 16.8475C16.5227 16.948 16.2626 16.9998 16 16.9998C15.7374 16.9998 15.4773 16.948 15.2346 16.8475C14.992 16.747 14.7715 16.5997 14.5858 16.414C14.4001 16.2283 14.2528 16.0078 14.1522 15.7651C14.0517 15.5225 14 15.2624 14 14.9998C14 14.4475 13.5523 13.9998 13 13.9998H5L9.2 8.39976ZM12.127 15.9998H4V21.9998C4 23.1043 4.89543 23.9998 6 23.9998H26C27.1046 23.9998 28 23.1043 28 21.9998V15.9998H19.873C19.8264 16.1802 19.7672 16.3576 19.6955 16.5305C19.4945 17.0158 19.1999 17.4567 18.8284 17.8282C18.457 18.1996 18.016 18.4943 17.5307 18.6953C17.0454 18.8963 16.5253 18.9998 16 18.9998C15.4747 18.9998 14.9546 18.8963 14.4693 18.6953C13.984 18.4943 13.543 18.1996 13.1716 17.8282C12.8001 17.4567 12.5055 17.0158 12.3045 16.5305C12.2329 16.3576 12.1736 16.1802 12.127 15.9998ZM10 5.99976C9.05573 5.99976 8.16656 6.44434 7.6 7.19976L2.2 14.3998C2.07018 14.5729 2 14.7834 2 14.9998V21.9998C2 24.2089 3.79086 25.9998 6 25.9998H26C28.2091 25.9998 30 24.2089 30 21.9998V14.9998C30 14.7834 29.9298 14.5729 29.8 14.3998L24.4 7.19976C23.8334 6.44434 22.9443 5.99976 22 5.99976H10Z"
/>
<symbol id="inbox" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 4.5C6.7918 4.5 6.12492 4.83344 5.7 5.4L1.65 10.8C1.55263 10.9298 1.5 11.0877 1.5 11.25V16.5C1.5 18.1569 2.84315 19.5 4.5 19.5H19.5C21.1569 19.5 22.5 18.1569 22.5 16.5V11.25C22.5 11.0877 22.4474 10.9298 22.35 10.8L18.3 5.4C17.8751 4.83344 17.2082 4.5 16.5 4.5H7.5ZM6.9 6.3C7.04164 6.11115 7.26393 6 7.5 6H16.5C16.7361 6 16.9584 6.11115 17.1 6.3L20.25 10.5H14.25C13.8358 10.5 13.5 10.8358 13.5 11.25C13.5 11.447 13.4612 11.642 13.3858 11.824C13.3104 12.006 13.1999 12.1714 13.0607 12.3107C12.9214 12.4499 12.756 12.5604 12.574 12.6358C12.392 12.7112 12.197 12.75 12 12.75C11.803 12.75 11.608 12.7112 11.426 12.6358C11.244 12.5604 11.0786 12.4499 10.9393 12.3107C10.8001 12.1714 10.6896 12.006 10.6142 11.824C10.5388 11.642 10.5 11.447 10.5 11.25C10.5 10.8358 10.1642 10.5 9.75 10.5H3.75L6.9 6.3ZM9.09526 12H3V16.5C3 17.3284 3.67157 18 4.5 18H19.5C20.3284 18 21 17.3284 21 16.5V12H14.9047C14.8698 12.1353 14.8254 12.2683 14.7716 12.3981C14.6209 12.762 14.3999 13.0927 14.1213 13.3713C13.8427 13.6499 13.512 13.8709 13.1481 14.0216C12.7841 14.1724 12.394 14.25 12 14.25C11.606 14.25 11.2159 14.1724 10.8519 14.0216C10.488 13.8709 10.1573 13.6499 9.87868 13.3713C9.6001 13.0927 9.37913 12.762 9.22836 12.3981C9.17464 12.2683 9.1302 12.1353 9.09526 12Z" />
</symbol>
<symbol id="track" viewBox="0 0 24 24">
<path d="M21.7,15L21,13.8c-0.6-1.1-1.1-2.3-1.2-3.4l-0.1-0.9c-0.5,0.2-1.1,0.4-1.6,0.4l0.1,0.7c0.1,1.5,0.6,2.9,1.4,4.2l0.7,1.2 c0.4,0.6,0.7,1.2,0.7,1.3c0,0.1-0.1,0.1-0.1,0.2c-0.1,0.1-1,0.1-1.5,0.1H4.8c-0.6,0-1.4,0-1.5-0.1l-0.1-0.1c0-0.1,0.5-0.8,0.7-1.4 l0.7-1.2c0.7-1.3,1.2-2.6,1.4-4.2l0.4-2.7c0.4-3,2.7-5.1,5.7-5.1c1,0,1.9,0.3,2.7,0.7c0.4-0.5,0.9-0.9,1.4-1.1C15,1.5,13.5,1,12,1 C8.2,1,5,3.9,4.5,7.7l-0.4,2.7c-0.1,1.2-0.5,2.4-1.2,3.4L2.2,15c-0.7,1.2-1.1,1.8-1,2.6c0.1,0.5,0.4,1,0.7,1.3 c0.6,0.5,1.3,0.5,2.7,0.5h3c0.2,1,0.8,1.9,1.5,2.5C10.1,22.6,11,23,12,23s2-0.4,2.7-1.1c0.7-0.6,1.2-1.5,1.5-2.5h3 c1.4,0,2.1,0,2.7-0.5c0.4-0.4,0.6-0.8,0.7-1.3C22.9,16.8,22.6,16.2,21.7,15z M13.6,20.6c-1,0.8-2.3,0.8-3.2,0 c-0.4-0.2-0.6-0.7-0.8-1.2h4.9C14.2,19.9,14,20.3,13.6,20.6z"/>

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -33,6 +33,8 @@
"RemovedCollaborators": "Removed collaborators",
"Edited": "edited",
"Pinned": "Pinned",
"Message": "Message"
"Message": "Message",
"FlatList": "Flat list",
"GroupedList": "Grouped list"
}
}

View File

@ -33,6 +33,8 @@
"RemovedCollaborators": "Удалены участники",
"Edited": "отредактировал(а)",
"Pinned": "Закреплено",
"Message": "Сообщение"
"Message": "Сообщение",
"FlatList": "Flat list",
"GroupedList": "Grouped list"
}
}

View File

@ -1,200 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import activity, { TxViewlet } from '@hcengineering/activity'
import { activityKey, ActivityKey } from '@hcengineering/activity-resources'
import { Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import notification, { DocUpdates } from '@hcengineering/notification'
import { ActionContext, createQuery } from '@hcengineering/presentation'
import { Loading, Scroller } from '@hcengineering/ui'
import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import NotificationView from './NotificationView.svelte'
export let filter: 'all' | 'read' | 'unread' = 'all'
export let _id: Ref<Doc> | undefined
const dispatch = createEventDispatcher()
const query = createQuery()
let docs: DocUpdates[] = []
let filtered: DocUpdates[] = []
let loading = true
let previousFilter: 'all' | 'read' | 'unread' = filter
$: query.query(
notification.class.DocUpdates,
{
user: getCurrentAccount()._id,
hidden: false
},
(res) => {
docs = res
getFiltered(docs, filter)
loading = false
},
{
sort: {
lastTxTime: -1
}
}
)
function getFiltered (docs: DocUpdates[], filter: 'all' | 'read' | 'unread'): void {
if (filter === 'read') {
filtered = docs.filter((p) => !p.txes.some((p) => p.isNew) && p.txes.length > 0)
} else if (filter === 'unread') {
const current = previousFilter === 'unread' ? new Set(filtered.map((p) => p._id)) : new Set()
filtered = docs.filter((p) => (current.has(p._id) || p.txes.some((p) => p.isNew)) && p.txes.length > 0)
} else {
filtered = docs.filter((p) => p.txes.length > 0)
}
previousFilter = filter
listProvider.update(filtered)
if (_id === undefined) {
changeSelected(selected)
} else {
const index = filtered.findIndex((p) => p.attachedTo === _id)
if (index === -1) {
changeSelected(selected)
} else {
selected = index
changeSelected(selected)
markAsRead(selected)
}
}
}
$: getFiltered(docs, filter)
function markAsRead (index: number) {
if (filtered[index] !== undefined) {
filtered[index].txes.forEach((p) => (p.isNew = false))
filtered[index].txes = filtered[index].txes
filtered = filtered
}
}
function changeSelected (index: number) {
if (filtered[index] !== undefined) {
listProvider.updateFocus(filtered[index])
_id = filtered[index]?.attachedTo
dispatch('change', filtered[index])
markAsRead(index)
} else if (filtered.length > 0) {
if (index < filtered.length - 1) {
selected++
} else {
selected--
}
changeSelected(selected)
} else {
selected = 0
_id = undefined
dispatch('change', undefined)
}
}
let viewlets: Map<ActivityKey, TxViewlet[]>
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {
let value = (of != null ? filtered.findIndex((p) => p._id === of._id) : selected) ?? -1
if (value === -1) {
// keep the current index if the document does not exist anymore
value = selected
} else {
value += offset
}
if (value < 0) {
value = 0
}
if (value >= filtered.length) {
value = filtered.length - 1
}
if (filtered[value] !== undefined) {
selected = value
changeSelected(selected)
}
}
})
const descriptors = createQuery()
descriptors.query(activity.class.TxViewlet, {}, (result) => {
viewlets = new Map()
for (const res of result) {
const key = activityKey(res.objectClass, res.txClass)
const arr = viewlets.get(key) ?? []
arr.push(res)
viewlets.set(key, arr)
}
viewlets = viewlets
})
let selected = 0
function onKeydown (key: KeyboardEvent): void {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
selected--
changeSelected(selected)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
selected++
changeSelected(selected)
}
// if (key.code === 'Backspace') {
// key.preventDefault()
// key.stopPropagation()
// hide(listProvider.docs()[selected] as DocUpdates)
// }
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
// dispatch('open', selected)
}
}
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<div class="inbox-activity py-2">
<Scroller noStretch>
{#if loading}
<Loading />
{:else}
{#each filtered as item, i (item._id)}
<NotificationView
value={item}
selected={selected === i}
{viewlets}
on:keydown={onKeydown}
on:click={() => {
selected = i
changeSelected(selected)
}}
/>
{/each}
{/if}
</Scroller>
</div>

View File

@ -0,0 +1,165 @@
<!--
// 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 { ActionIcon, CheckBox, Component, IconMoreH, Label, showPopup } from '@hcengineering/ui'
import notification, {
ActivityNotificationViewlet,
DisplayInboxNotification,
DocNotifyContext
} 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 InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte'
import NotifyContextIcon from './NotifyContextIcon.svelte'
import NotifyMarker from './NotifyMarker.svelte'
export let value: DocNotifyContext
export let notifications: DisplayInboxNotification[] = []
export let viewlets: ActivityNotificationViewlet[] = []
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
$: visibleNotification = notifications[0]
function showMenu (ev: MouseEvent): void {
showPopup(
Menu,
{
object: value,
baseMenuClass: notification.class.DocNotifyContext,
excludedActions: [
notification.action.PinDocNotifyContext,
notification.action.UnpinDocNotifyContext,
chunter.action.OpenChannel
]
},
ev.target as HTMLElement
)
}
const presenterMixin = hierarchy.classHierarchyMixin(
value.attachedToClass,
notification.mixin.NotificationContextPresenter
)
</script>
{#if visibleNotification}
{@const unreadCount = notifications.filter(({ isViewed }) => !isViewed).length}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="card"
on:click={() => {
dispatch('click', { context: value, notification: visibleNotification })
}}
>
<div class="header">
<CheckBox
circle
kind="primary"
on:value={(event) => {
dispatch('check', event.detail)
}}
/>
<NotifyContextIcon {value} />
{#if presenterMixin?.labelPresenter}
<Component is={presenterMixin.labelPresenter} props={{ notification: visibleNotification, context: value }} />
{:else}
<div class="labels">
{#await getDocIdentifier(client, value.attachedTo, value.attachedToClass) then title}
{#if title}
{title}
{:else}
<Label label={hierarchy.getClass(value.attachedToClass).label} />
{/if}
{/await}
{#await getDocTitle(client, value.attachedTo, value.attachedToClass) then title}
<div class="title overflow-label" {title}>
{title ?? hierarchy.getClass(value.attachedToClass).label}
</div>
{/await}
</div>
{/if}
<div class="actions">
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
</div>
<div class="notifyMarker">
<NotifyMarker count={unreadCount} />
</div>
</div>
<div class="notification">
<InboxNotificationPresenter value={visibleNotification} {viewlets} embedded skipLabel />
</div>
</div>
{/if}
<style lang="scss">
.card {
display: flex;
flex-direction: column;
cursor: pointer;
border: 1px solid transparent;
border-radius: 0.5rem;
padding: 1rem;
margin: 0.5rem 0;
.header {
position: relative;
display: flex;
align-items: center;
gap: 1rem;
}
.title {
font-weight: 500;
max-width: 20.5rem;
}
&:hover {
background-color: var(--highlight-hover);
}
}
.labels {
display: flex;
flex-direction: column;
}
.notification {
margin-top: 1rem;
margin-left: 5.5rem;
}
.notifyMarker {
position: absolute;
right: 1.875rem;
top: 0;
}
.actions {
position: absolute;
right: 0;
top: 0;
}
</style>

View File

@ -1,194 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import chunter, { getDirectChannel } from '@hcengineering/chunter'
import contact, { Employee, PersonAccount } from '@hcengineering/contact'
import { Class, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
import { DocUpdates } from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
import {
AnyComponent,
Button,
Component,
IconAdd,
Tabs,
eventToHTMLElement,
getLocation,
navigate,
showPopup,
defineSeparators,
Separator
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { UsersPopup } from '@hcengineering/contact-resources'
import notification from '../plugin'
import Activity from './Activity.svelte'
import EmployeeInbox from './EmployeeInbox.svelte'
import Filter from './Filter.svelte'
import People from './People.svelte'
import { subscribe } from '../utils'
export let visibleNav: boolean = true
export let navFloat: boolean = false
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
let filter: 'all' | 'read' | 'unread' = 'all'
const client = getClient()
const hierarchy = client.getHierarchy()
$: tabs = [
{
label: notification.string.Activity,
props: { filter, _id },
component: Activity
},
{
label: notification.string.People,
props: { filter, _id },
component: People
}
]
let component: AnyComponent | undefined
let _id: Ref<Doc> | undefined
let _class: Ref<Class<Doc>> | undefined
let selectedEmployee: Ref<PersonAccount> | undefined = undefined
async function select (value: DocUpdates | undefined) {
if (!value) {
component = undefined
_id = undefined
_class = undefined
return
}
const isDmOpened = hierarchy.isDerived(value.attachedToClass, chunter.class.ChunterSpace)
if (!isDmOpened && value !== undefined) {
// chats messages are marked as read explicitly, but
// other notifications should be marked as read upon opening
if (value.txes.some((p) => p.isNew)) {
value.txes.forEach((p) => (p.isNew = false))
const txes = value.txes
await client.update(value, { txes })
}
}
if (hierarchy.isDerived(value.attachedToClass, chunter.class.ChunterSpace)) {
openDM(value.attachedTo)
} else {
const panelComponent = hierarchy.classHierarchyMixin(value.attachedToClass, view.mixin.ObjectPanel)
component = panelComponent?.component ?? view.component.EditDoc
_id = value.attachedTo
_class = value.attachedToClass
}
}
function openDM (value: Ref<Doc>) {
if (value) {
const panelComponent = hierarchy.classHierarchyMixin(
chunter.class.DirectMessage as Ref<Class<Doc>>,
view.mixin.ObjectPanel
)
component = panelComponent?.component ?? view.component.EditDoc
_id = value
_class = chunter.class.DirectMessage
const loc = getLocation()
loc.path[3] = _id
navigate(loc)
}
}
let selectedTab = 0
const me = getCurrentAccount() as PersonAccount
function openUsersPopup (ev: MouseEvent) {
showPopup(
UsersPopup,
{ _class: contact.mixin.Employee, docQuery: { _id: { $ne: me.person } } },
eventToHTMLElement(ev),
async (employee: Employee) => {
if (employee != null) {
const personAccount = await client.findOne(contact.class.PersonAccount, { person: employee._id })
if (personAccount !== undefined) {
const channel = await getDirectChannel(client, me._id, personAccount._id)
// re-subscribing in case DM was removed from notifications
await subscribe(chunter.class.DirectMessage, channel)
openDM(channel)
}
}
}
)
}
defineSeparators('inbox', [
{ minSize: 20, maxSize: 40, size: 30, float: 'navigator' },
{ size: 'auto', minSize: 30, maxSize: 'auto', float: undefined }
])
</script>
<div class="flex-row-top h-full">
{#if visibleNav}
<div
class="antiPanel-navigator {appsDirection === 'horizontal'
? 'portrait'
: 'landscape'} background-comp-header-color"
>
<div class="antiPanel-wrap__content">
<Tabs
bind:selected={selectedTab}
model={tabs}
on:change={(e) => select(e.detail)}
on:open={(e) => {
selectedEmployee = e.detail
select(undefined)
}}
padding={'0 1.75rem'}
size={'small'}
noMargin
>
<svelte:fragment slot="rightButtons">
<div class="flex flex-gap-2">
{#if selectedTab > 0}
<Button label={chunter.string.Message} icon={IconAdd} kind="primary" on:click={openUsersPopup} />
{/if}
<Filter bind:filter />
</div>
</svelte:fragment>
</Tabs>
</div>
<Separator name={'inbox'} float={navFloat ? 'navigator' : true} index={0} />
</div>
<Separator name={'inbox'} float={navFloat} index={0} />
{/if}
<div class="antiPanel-component filled w-full">
{#if selectedEmployee !== undefined && component === undefined}
<EmployeeInbox
accountId={selectedEmployee}
on:change={(e) => select(e.detail)}
on:dm={(e) => {
openDM(e.detail)
}}
on:close={() => {
selectedEmployee = undefined
}}
/>
{:else if component && _id && _class}
<Component is={component} props={{ _id, _class, embedded: true }} on:close={() => select(undefined)} />
{/if}
</div>
</div>

View File

@ -1,121 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { TxViewlet } from '@hcengineering/activity'
import { ActivityKey } from '@hcengineering/activity-resources'
import core, { Doc, TxCUD, TxProcessor } from '@hcengineering/core'
import notification, { DocUpdates } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { AnySvelteComponent, TimeSince, getEventPositionElement, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Menu } from '@hcengineering/view-resources'
import TxView from './TxView.svelte'
export let value: DocUpdates
export let viewlets: Map<ActivityKey, TxViewlet[]>
export let selected: boolean
export let preview: boolean = false
let doc: Doc | undefined = undefined
let tx: TxCUD<Doc> | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
$: txRef = value.txes[value.txes.length - 1]._id
$: txRef &&
client.findOne(core.class.TxCUD, { _id: txRef }).then((res) => {
if (res !== undefined) {
tx = TxProcessor.extractTx(res) as TxCUD<Doc>
} else {
tx = res
}
})
let presenter: AnySvelteComponent | undefined = undefined
$: presenterRes =
hierarchy.classHierarchyMixin(value.attachedToClass, notification.mixin.NotificationObjectPresenter)?.presenter ??
hierarchy.classHierarchyMixin(value.attachedToClass, view.mixin.ObjectPresenter)?.presenter
$: if (presenterRes) {
getResource(presenterRes).then((res) => (presenter = res))
}
const docQuery = createQuery()
$: docQuery.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
;[doc] = res
})
$: newTxes = value.txes.filter((p) => p.isNew).length
function showMenu (e: MouseEvent) {
showPopup(Menu, { object: value, baseMenuClass: value._class }, getEventPositionElement(e))
}
let div: HTMLDivElement
$: if (selected && div !== undefined) div.focus()
let notificationPreviewPresenter: AnySvelteComponent | undefined = undefined
$: notificationPreviewPresenterRes = hierarchy.classHierarchyMixin(
value.attachedToClass,
notification.mixin.NotificationPreview
)?.presenter
$: if (notificationPreviewPresenterRes) {
getResource(notificationPreviewPresenterRes).then((res) => (notificationPreviewPresenter = res))
}
let object: Doc | undefined
const objQuery = createQuery()
$: objQuery.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
;[object] = res
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if doc}
<div bind:this={div} class="inbox-activity__container" class:selected tabindex="-1" on:keydown on:click>
{#if newTxes > 0 && !selected}<div class="notify" />{/if}
<div class="inbox-activity__content" class:read={newTxes === 0} on:contextmenu|preventDefault={showMenu}>
<div class="flex-row-center flex-no-shrink mr-8">
{#if presenter}
<svelte:component this={presenter} value={doc} accent disabled inbox />
{/if}
{#if newTxes > 0 && !selected}
<div class="counter float">{newTxes}</div>
{/if}
</div>
{#if preview && object && notificationPreviewPresenter !== undefined}
<div class="mt-2">
<svelte:component this={notificationPreviewPresenter} {object} {newTxes} />
</div>
{/if}
{#if !preview || notificationPreviewPresenter === undefined}
<div class="flex-between flex-baseline mt-3">
<div>
{#if tx}
<TxView {tx} {viewlets} objectId={value.attachedTo} />
{/if}
</div>
<div class="time">
<TimeSince value={tx?.modifiedOn} />
</div>
</div>
{/if}
</div>
</div>
{/if}

View File

@ -0,0 +1,56 @@
<!--
// 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 notification, { DocNotifyContext } from '@hcengineering/notification'
import { Component, Icon, IconSize } from '@hcengineering/ui'
import { createQuery, getClient } from '@hcengineering/presentation'
import { classIcon } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import { Doc } from '@hcengineering/core'
export let value: DocNotifyContext
const client = getClient()
const hierarchy = client.getHierarchy()
const query = createQuery()
let object: Doc | undefined = undefined
$: iconMixin = hierarchy.classHierarchyMixin(value.attachedToClass, view.mixin.ObjectIcon)
$: iconMixin &&
query.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
object = res[0]
})
</script>
<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" />
{/if}
</div>
<style lang="scss">
.container {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 0.5rem;
background-color: var(--theme-button-hovered);
width: 2.5rem;
height: 2.5rem;
}
</style>

View File

@ -0,0 +1,44 @@
<!--
// 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">
export let count: number = 0
const maxNumber = 9
</script>
{#if count > 0}
<div class="notifyMarker">
{#if count > maxNumber}
{maxNumber}+
{:else}
{count}
{/if}
</div>
{/if}
<style lang="scss">
.notifyMarker {
display: flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
border-radius: 50%;
background-color: var(--highlight-red);
font-size: 0.625rem;
font-weight: 700;
color: var(--white-color);
}
</style>

View File

@ -1,258 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { DisplayTx, TxViewlet } from '@hcengineering/activity'
import {
ActivityKey,
TxDisplayViewlet,
getValue,
newDisplayTx,
updateViewlet
} from '@hcengineering/activity-resources'
import activity from '@hcengineering/activity-resources/src/plugin'
import attachment from '@hcengineering/attachment'
import chunter from '@hcengineering/chunter'
import { Person, PersonAccount } from '@hcengineering/contact'
import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import core, { AnyAttribute, Class, Doc, Ref, TxCUD } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { AnyComponent, Component, Icon, IconActivityEdit, Label } from '@hcengineering/ui'
import type { AttributeModel } from '@hcengineering/view'
import { ObjectPresenter } from '@hcengineering/view-resources'
export let tx: TxCUD<Doc>
export let objectId: Ref<Doc>
export let viewlets: Map<ActivityKey, TxViewlet[]>
const client = getClient()
let ptx: DisplayTx | undefined
let viewlet: TxDisplayViewlet | undefined
let props: any
let account: PersonAccount | undefined
let employee: Person | undefined
let model: AttributeModel[] = []
let iconComponent: AnyComponent | undefined = undefined
let modelIcon: Asset | undefined = undefined
$: if (tx._id !== ptx?.tx._id) {
ptx = newDisplayTx(tx, client.getHierarchy(), objectId === tx.objectId)
if (tx.modifiedBy !== account?._id) {
account = undefined
employee = undefined
}
props = undefined
viewlet = undefined
model = []
}
function getProps (props: any): any {
return { ...props, attr: ptx?.collectionAttribute }
}
$: ptx &&
updateViewlet(client, viewlets, ptx).then((result) => {
if (result.id === tx._id) {
viewlet = result.viewlet
modelIcon = result.modelIcon
iconComponent = result.iconComponent
props = getProps(result.props)
model = result.model
}
})
$: account = $personAccountByIdStore.get(tx.modifiedBy as Ref<PersonAccount>)
$: employee = account ? $personByIdStore.get(account?.person) : undefined
function isMessageType (attr?: AnyAttribute): boolean {
return attr?.type._class === core.class.TypeMarkup
}
async function updateMessageType (model: AttributeModel[], tx: DisplayTx): Promise<boolean> {
for (const m of model) {
if (isMessageType(m.attribute)) {
return true
}
const val = await getValue(client, m, tx)
if (val.added.length > 1 || val.removed.length > 1) {
return true
}
}
return false
}
let hasMessageType = false
$: ptx &&
updateMessageType(model, ptx).then((res) => {
hasMessageType = res
})
$: isComment = viewlet && viewlet?.editable
$: isAttached = isAttachment(tx)
$: isMentioned = isMention(tx.objectClass)
$: withAvatar = isComment || isMentioned || isAttached
$: isEmphasized = viewlet?.display === 'emphasized' || model.every((m) => isMessageType(m.attribute))
$: isColumn = isComment || isEmphasized || hasMessageType
function isAttachment (tx: TxCUD<Doc>): boolean {
return tx.objectClass === attachment.class.Attachment && tx._class === core.class.TxCreateDoc
}
function isMention (_class?: Ref<Class<Doc>>): boolean {
return _class === chunter.class.Backlink
}
</script>
{#if (viewlet !== undefined && !((viewlet?.hideOnRemove ?? false) && ptx?.removed)) || model.length > 0}
<div class="msgactivity-container">
{#if withAvatar}
<div class="msgactivity-avatar">
<Avatar avatar={employee?.avatar} size={'x-small'} name={employee?.name} />
</div>
{:else}
<div class="msgactivity-icon">
{#if iconComponent}
<Component is={iconComponent} {props} />
{:else if viewlet}
<Icon icon={viewlet.icon} size="medium" />
{:else if viewlet === undefined && model.length > 0}
<Icon icon={modelIcon !== undefined ? modelIcon : IconActivityEdit} size="medium" />
{:else}
<Icon icon={IconActivityEdit} size="medium" />
{/if}
</div>
{/if}
<div class="msgactivity-content" class:content={isColumn} class:comment={isComment}>
<div class="msgactivity-content__title labels-row">
{#if viewlet && viewlet?.editable}
{#if viewlet.label}
<span class="lower"><Label label={viewlet.label} params={viewlet.labelParams ?? {}} /></span>
{/if}
{#if ptx?.updated}
<span class="lower"><Label label={activity.string.Edited} /></span>
{/if}
{:else if viewlet && viewlet.label}
<span class="lower whitespace-nowrap">
<Label label={viewlet.label} params={viewlet.labelParams ?? {}} />
</span>
{/if}
{#if viewlet && viewlet.labelComponent}
<Component is={viewlet.labelComponent} {props} />
{/if}
{#if viewlet === undefined && model.length > 0 && ptx?.updateTx}
{#each model as m}
{#await getValue(client, m, ptx) then value}
{#if value.added.length}
<span class="lower"><Label label={activity.string.Added} /></span>
<span class="lower"><Label label={activity.string.To} /></span>
<span class="lower"><Label label={m.label} /></span>
{#each value.added as cvalue}
{#if value.isObjectAdded}
<ObjectPresenter value={cvalue} />
{:else}
<svelte:component this={m.presenter} value={cvalue} />
{/if}
{/each}
{:else if value.removed.length}
<span class="lower"><Label label={activity.string.Removed} /></span>
<span class="lower"><Label label={activity.string.From} /></span>
<span class="lower"><Label label={m.label} /></span>
{#each value.removed as cvalue}
{#if value.isObjectRemoved}
<ObjectPresenter value={cvalue} />
{:else}
<svelte:component this={m.presenter} value={cvalue} />
{/if}
{/each}
{:else if value.set === null || value.set === undefined || value.set === ''}
<span class="lower"><Label label={activity.string.Unset} /></span>
<span class="lower"><Label label={m.label} /></span>
{:else}
<span class="lower"><Label label={activity.string.Changed} /></span>
<span class="lower"><Label label={m.label} /></span>
<span class="lower"><Label label={activity.string.To} /></span>
{#if !hasMessageType}
<span class="strong">
{#if value.isObjectSet}
<ObjectPresenter value={value.set} accent />
{:else}
<svelte:component this={m.presenter} value={value.set} accent />
{/if}
</span>
{/if}
{/if}
{/await}
{/each}
{:else if viewlet === undefined && model.length > 0 && ptx?.mixinTx}
{#each model as m}
{#await getValue(client, m, ptx) then value}
{#if value.set === null || value.set === ''}
<span class="lower"><Label label={activity.string.Unset} /></span>
<span class="lower"><Label label={m.label} /></span>
{:else}
<span class="lower"><Label label={activity.string.Changed} /></span>
<span class="lower"><Label label={m.label} /></span>
<span class="lower"><Label label={activity.string.To} /></span>
{#if !hasMessageType}
<div class="strong">
{#if value.isObjectSet}
<ObjectPresenter value={value.set} accent />
{:else}
<svelte:component this={m.presenter} value={value.set} accent />
{/if}
</div>
{/if}
{/if}
{/await}
{/each}
{:else if viewlet && viewlet.display === 'inline' && viewlet.component}
{#if typeof viewlet.component === 'string'}
<Component is={viewlet.component} {props} />
{:else}
<svelte:component this={viewlet.component} {...props} />
{/if}
{/if}
</div>
</div>
</div>
{/if}
<style lang="scss">
.msgactivity-container {
display: flex;
align-items: center;
min-width: 0;
min-height: 0;
.msgactivity-content {
display: flex;
flex-grow: 1;
margin-left: 0.5rem;
margin-right: 1rem;
min-width: 0;
min-height: 0;
}
}
.msgactivity-icon,
.msgactivity-avatar {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
min-width: 0;
}
</style>

View File

@ -14,22 +14,28 @@
-->
<script lang="ts">
import { createQuery, getClient } from '@hcengineering/presentation'
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
import { matchQuery, Ref, SortingOrder } from '@hcengineering/core'
import notification, {
ActivityInboxNotification,
ActivityNotificationViewlet,
DisplayActivityInboxNotification,
InboxNotification
} from '@hcengineering/notification'
import { ActivityMessagePresenter, combineActivityMessages } from '@hcengineering/activity-resources'
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import { getLocation, location, navigate, Action } from '@hcengineering/ui'
import { location, Action, CheckBox, getLocation, navigate, Component } from '@hcengineering/ui'
import { getActions } from '@hcengineering/view-resources'
import { getResource } from '@hcengineering/platform'
import chunter from '@hcengineering/chunter'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import { getActions } from '@hcengineering/view-resources'
import { getResource } from '@hcengineering/platform'
export let value: DisplayActivityInboxNotification
export let embedded = false
export let skipLabel = false
export let viewlets: ActivityNotificationViewlet[] = []
export let onClick: (() => void) | undefined = undefined
export let onCheck: ((isChecked: boolean) => void) | undefined = undefined
const client = getClient()
const messagesQuery = createQuery()
@ -37,6 +43,7 @@
const notificationsStore = notificationsClient.inboxNotifications
let messages: ActivityMessage[] = []
let viewlet: ActivityNotificationViewlet | undefined = undefined
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
let displayMessage: DisplayActivityMessage | undefined = undefined
let actions: Action[] = []
@ -65,45 +72,33 @@
$: displayMessage = messages.length > 1 ? combineActivityMessages(messages)[0] : messages[0]
function handleMessageClicked (message?: ActivityMessage): void {
if (message === undefined) {
return
}
if (message._class === chunter.class.ThreadMessage) {
openDocActivity(message._id, true)
selectedMessageId = message._id
} else if (client.getHierarchy().isDerived(message.attachedToClass, activity.class.ActivityMessage)) {
openDocActivity(message.attachedTo, false)
selectedMessageId = message.attachedTo as Ref<ActivityMessage>
} else {
openDocActivity(message._id, false)
selectedMessageId = message._id
}
markNotificationViewed()
}
$: getAllActions(value).then((res) => {
actions = res
})
function handleReply (message?: ActivityMessage): void {
if (message === undefined) {
$: updateViewlet(viewlets, displayMessage)
function updateViewlet (viewlets: ActivityNotificationViewlet[], message?: DisplayActivityMessage) {
if (viewlets.length === 0 || message === undefined) {
return
}
openDocActivity(message._id, true)
selectedMessageId = message._id
for (const v of viewlets) {
const matched = matchQuery([message], v.messageMatch, message._class, client.getHierarchy(), true)
if (matched.length > 0) {
viewlet = v
return
}
}
}
function markNotificationViewed () {
combinedNotifications.forEach((notification) => {
client.update(notification, { isViewed: true })
})
}
function openDocActivity (_id: Ref<Doc>, thread: boolean) {
function handleReply (message?: DisplayActivityMessage): void {
if (message === undefined) {
return
}
const loc = getLocation()
loc.path[4] = _id
loc.query = {
...loc.query,
thread: `${thread}`
}
loc.fragment = value.docNotifyContext
loc.query = { message: message._id }
navigate(loc)
}
@ -122,24 +117,46 @@
return result
}
$: getAllActions(value).then((res) => {
actions = res
})
</script>
{#if displayMessage !== undefined}
<ActivityMessagePresenter
value={displayMessage}
showNotify={!value.isViewed}
isSelected={displayMessage._id === selectedMessageId}
showEmbedded
{actions}
onReply={() => {
handleReply(displayMessage)
}}
onClick={() => {
handleMessageClicked(displayMessage)
}}
/>
<div class="notification gap-2 ml-2">
{#if !embedded}
<div class="mt-6">
<CheckBox
circle
kind="primary"
on:value={(event) => {
if (onCheck) {
onCheck(event.detail)
}
}}
/>
</div>
{/if}
{#if viewlet}
<Component is={viewlet.presenter} props={{ message: displayMessage, notification: value, embedded, onClick }} />
{:else}
<ActivityMessagePresenter
value={displayMessage}
showNotify={!value.isViewed && !embedded}
isSelected={displayMessage._id === selectedMessageId}
excludedActions={[chunter.action.ReplyToThread]}
showEmbedded
{embedded}
{skipLabel}
{actions}
onReply={() => {
handleReply(displayMessage)
}}
{onClick}
/>
{/if}
</div>
{/if}
<style lang="scss">
.notification {
display: flex;
}
</style>

View File

@ -26,7 +26,7 @@
import { translate } from '@hcengineering/platform'
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import notification, { CommonInboxNotification } from '@hcengineering/notification'
import { ActionIcon, IconMoreH, Label, showPopup } from '@hcengineering/ui'
import { ActionIcon, CheckBox, IconMoreH, Label, showPopup } from '@hcengineering/ui'
import { getDocLinkTitle, Menu } from '@hcengineering/view-resources'
import { ActivityDocLink } from '@hcengineering/activity-resources'
import activity from '@hcengineering/activity'
@ -35,6 +35,9 @@
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
export let value: CommonInboxNotification
export let embedded = false
export let onClick: (() => void) | undefined = undefined
export let onCheck: ((isChecked: boolean) => void) | undefined = undefined
const objectQuery = createQuery()
const client = getClient()
@ -89,52 +92,74 @@
}
</script>
<div class="root clear-mins">
{#if !value.isViewed}
<div class="notify" />
<div class="flex-presenter gap-2 ml-2">
{#if !embedded}
<CheckBox
circle
kind="primary"
on:value={(event) => {
if (onCheck) {
onCheck(event.detail)
}
}}
/>
{/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}
<div class="content ml-2 w-full clear-mins">
<div class="header clear-mins">
{#if person}
<EmployeePresenter value={person} shouldShowAvatar={false} />
<!-- 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}
<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}
<div class="strong">
<Label label={core.string.System} />
<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 value.header}
<span class="text-sm lower"><Label label={value.header} /></span>
{/if}
{#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}
<span class="text-sm">{getDisplayTime(value.createdOn ?? 0)}</span>
</div>
<div class="flex-row-center">
<div class="customContent">
<MessageViewer message={content} />
</div>
{/if}
{#if value.header}
<span class="text-sm lower"><Label label={value.header} /></span>
{/if}
{#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}
<span class="text-sm">{getDisplayTime(value.createdOn ?? 0)}</span>
</div>
<div class="flex-row-center">
<div class="customContent">
<MessageViewer message={content} />
</div>
</div>
</div>
<div class="actions clear-mins flex flex-gap-2 items-center" class:opened={isActionMenuOpened}>
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
{#if !embedded}
<div class="actions clear-mins flex flex-gap-2 items-center" class:opened={isActionMenuOpened}>
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
</div>
{/if}
</div>
</div>
@ -144,9 +169,10 @@
display: flex;
flex-shrink: 0;
padding: 0.75rem 0.75rem 0.75rem 1.25rem;
border-radius: 8px;
border-radius: 0.5rem;
gap: 1rem;
overflow: hidden;
cursor: pointer;
.actions {
position: absolute;
@ -200,4 +226,10 @@
background-color: var(--theme-inbox-notify);
border-radius: 50%;
}
.embeddedMarker {
width: 0.375rem;
border-radius: 0.5rem;
background: var(--secondary-button-default);
}
</style>

View File

@ -13,30 +13,195 @@
// limitations under the License.
-->
<script lang="ts">
import { DisplayInboxNotification } from '@hcengineering/notification'
import { ActionContext } from '@hcengineering/presentation'
import { Class, Doc, Ref } from '@hcengineering/core'
import { Label, Scroller } from '@hcengineering/ui'
import { IntlString } from '@hcengineering/platform'
import notification, {
ActivityNotificationViewlet,
DisplayInboxNotification,
DocNotifyContext
} from '@hcengineering/notification'
import { ActionContext, getClient } from '@hcengineering/presentation'
import view, { Viewlet } from '@hcengineering/view'
import {
AnyComponent,
Component,
defineSeparators,
getLocation,
Label,
Loading,
location as locationStore,
navigate,
Scroller,
Separator,
TabItem,
TabList
} from '@hcengineering/ui'
import chunter from '@hcengineering/chunter'
import { Ref, WithLookup } from '@hcengineering/core'
import { ViewletSelector } from '@hcengineering/view-resources'
import activity from '@hcengineering/activity'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import Filter from '../Filter.svelte'
import { getDisplayInboxNotifications } from '../../utils'
import { InboxNotificationsFilter } from '../../types'
import InboxNotificationPresenter from './InboxNotificationPresenter.svelte'
export let label: IntlString
export let _class: Ref<Class<Doc>> | undefined = undefined
export let visibleNav: boolean = true
export let navFloat: boolean = false
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
const client = getClient()
const hierarchy = client.getHierarchy()
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
const notificationsByContextStore = inboxClient.inboxNotificationsByContext
const notifyContextsStore = inboxClient.docNotifyContexts
const checkedContexts = new Set<Ref<DocNotifyContext>>()
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 filteredNotifications: DisplayInboxNotification[] = []
let filter: InboxNotificationsFilter = 'all'
$: getDisplayInboxNotifications($inboxNotificationsByContextStore, filter, _class).then((res) => {
let tabItems: TabItem[] = []
let displayContextsIds = new Set<Ref<DocNotifyContext>>()
let selectedTabId: string = allTab.id
let selectedContextId: Ref<DocNotifyContext> | undefined = undefined
let selectedContext: DocNotifyContext | undefined = undefined
let selectedComponent: AnyComponent | undefined = undefined
let viewlets: ActivityNotificationViewlet[] = []
let viewlet: WithLookup<Viewlet> | undefined
let loading = true
client.findAll(notification.class.ActivityNotificationViewlet, {}).then((res) => {
viewlets = res
})
$: getDisplayInboxNotifications($notificationsByContextStore, filter).then((res) => {
displayNotifications = res
})
locationStore.subscribe((newLocation) => {
selectedContextId = newLocation.fragment as Ref<DocNotifyContext> | undefined
if (selectedContextId !== selectedContext?._id) {
selectedContext = undefined
}
})
$: selectedContext = selectedContextId
? selectedContext ?? $notifyContextsStore.find(({ _id }) => _id === selectedContextId)
: undefined
$: displayContextsIds = new Set(displayNotifications.map(({ docNotifyContext }) => docNotifyContext))
$: updateSelectedPanel(selectedContext)
$: updateTabItems(displayContextsIds, $notifyContextsStore)
$: filteredNotifications = filterNotifications(selectedTabId, displayNotifications, $notifyContextsStore)
function updateTabItems (displayContextsIds: Set<Ref<DocNotifyContext>>, notifyContexts: DocNotifyContext[]): void {
const displayClasses = new Set(
notifyContexts
.filter(
({ _id, attachedToClass }) =>
displayContextsIds.has(_id) && !hierarchy.isDerived(attachedToClass, activity.class.ActivityMessage)
)
.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)
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
}))
)
}
function selectTab (event: CustomEvent) {
if (event.detail !== undefined) {
selectedTabId = event.detail.id
}
}
async function selectContext (event?: CustomEvent) {
selectedContext = event?.detail?.context
selectedContextId = selectedContext?._id
const loc = getLocation()
if (selectedContext !== undefined) {
loc.fragment = selectedContext._id
loc.query = { message: event?.detail?.notification?.attachedTo }
} else {
loc.fragment = undefined
loc.query = undefined
}
navigate(loc)
}
async function updateSelectedPanel (selectedContext?: DocNotifyContext) {
if (selectedContext === undefined) {
selectedComponent = undefined
return
}
const isChunterChannel = hierarchy.isDerived(selectedContext.attachedToClass, chunter.class.ChunterSpace)
const panelComponent = hierarchy.classHierarchyMixin(selectedContext.attachedToClass, view.mixin.ObjectPanel)
selectedComponent = panelComponent?.component ?? view.component.EditDoc
const contextNotifications = $notificationsByContextStore.get(selectedContext._id) ?? []
await inboxClient.readNotifications(
contextNotifications
.filter(({ _class, isViewed }) =>
isChunterChannel ? _class === notification.class.CommonInboxNotification : !isViewed
)
.map(({ _id }) => _id)
)
}
function filterNotifications (
selectedTabId: string,
displayNotifications: DisplayInboxNotification[],
notifyContexts: DocNotifyContext[]
) {
if (selectedTabId === allTab.id) {
return displayNotifications
}
return displayNotifications.filter(({ docNotifyContext }) => {
const context = notifyContexts.find(({ _id }) => _id === docNotifyContext)
return context !== undefined && context.attachedToClass === selectedTabId
})
}
defineSeparators('inbox', [
{ minSize: 30, maxSize: 50, size: 40, float: 'navigator' },
{ size: 'auto', minSize: 30, maxSize: 'auto', float: undefined }
])
</script>
<ActionContext
@ -45,25 +210,77 @@
}}
/>
<div class="ac-header full divide caption-height">
<div class="ac-header__wrap-title mr-3">
<span class="ac-header__title"><Label {label} /></span>
</div>
<div class="flex flex-gap-2">
<Filter bind:filter />
<div class="flex-row-top h-full">
{#if visibleNav}
<div
class="antiPanel-navigator {appsDirection === 'horizontal'
? 'portrait'
: 'landscape'} background-comp-header-color"
>
<div class="antiPanel-wrap__content">
<div class="ac-header full divide caption-height">
<div class="ac-header__wrap-title mr-3">
<span class="ac-header__title"><Label label={notification.string.Inbox} /></span>
</div>
<ViewletSelector bind:viewlet bind:loading viewletQuery={{ attachTo: notification.class.DocNotifyContext }} />
<div class="flex flex-gap-2">
<Filter bind:filter />
</div>
</div>
<div class="tabs">
<TabList items={tabItems} selected={selectedTabId} on:select={selectTab} />
</div>
{#if loading || !viewlet?.$lookup?.descriptor}
<Loading />
{:else if viewlet}
<Scroller>
<div class="notifications">
<Component
is={viewlet.$lookup.descriptor.component}
props={{
notifications: filteredNotifications,
checkedContexts,
viewlets
}}
on:click={selectContext}
/>
</div>
</Scroller>
{/if}
</div>
<Separator name="inbox" float={navFloat ? 'navigator' : true} index={0} />
</div>
<Separator name="inbox" float={navFloat} index={0} />
{/if}
<div class="antiPanel-component filled w-full">
{#if selectedContext && selectedComponent}
<Component
is={selectedComponent}
props={{
_id: selectedContext.attachedTo,
_class: selectedContext.attachedToClass,
embedded: true,
context: selectedContext,
props: { context: selectedContext }
}}
on:close={() => selectContext(undefined)}
/>
{/if}
</div>
</div>
<Scroller>
<div class="content">
{#each displayNotifications as notification}
<InboxNotificationPresenter value={notification} />
{/each}
</div>
</Scroller>
<style lang="scss">
.content {
padding: 0 24px;
.tabs {
display: flex;
margin: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--theme-navpanel-border);
}
.notifications {
margin: 0 0.5rem;
height: 100%;
}
</style>

View File

@ -1,144 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Class, Doc, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Component, getLocation, IconClose, location as locationStore, Spinner } from '@hcengineering/ui'
import view from '@hcengineering/view'
import activity, { ActivityMessage } from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import { buildRemovedDoc, checkIsObjectRemoved } from '@hcengineering/view-resources'
import { DocNotifyContext } from '@hcengineering/notification'
import { ActivityScrolledView } from '@hcengineering/activity-resources'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
export let _id: Ref<ActivityMessage>
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
const inboxClient = InboxNotificationsClientImpl.getClient()
const selectedMessageQuery = createQuery()
const objectQuery = createQuery()
let loc = getLocation()
let selectedMessage: ActivityMessage | undefined = undefined
let object: Doc | undefined = undefined
let isLoading: boolean = true
let notifyContext: DocNotifyContext | undefined = undefined
locationStore.subscribe((newLocation) => {
loc = newLocation
})
$: docNotifyContextByDocStore = inboxClient.docNotifyContextByDoc
$: selectedMessageQuery.query(
activity.class.ActivityMessage,
{ _id },
(result: ActivityMessage[]) => {
selectedMessage = result[0]
notifyContext = $docNotifyContextByDocStore.get(selectedMessage.attachedTo)
},
{
limit: 1
}
)
async function loadObject (_id: Ref<Doc>, _class: Ref<Class<Doc>>) {
const isRemoved = await checkIsObjectRemoved(client, _id, _class)
if (isRemoved) {
object = await buildRemovedDoc(client, _id, _class)
} else {
objectQuery.query(_class, { _id }, (res) => {
object = res[0]
})
}
}
$: selectedMessage && loadObject(selectedMessage.attachedTo, selectedMessage.attachedToClass)
$: objectPresenter =
selectedMessage && hierarchy.classHierarchyMixin(selectedMessage.attachedToClass, view.mixin.ObjectPresenter)
$: isThread = loc.query?.thread === 'true'
$: threadId =
selectedMessage?._class === chunter.class.ThreadMessage ? selectedMessage.attachedTo : selectedMessage?._id
</script>
{#if isThread && selectedMessage}
<Component
is={chunter.component.ThreadView}
props={{ _id: threadId, selectedMessageId: selectedMessage._id }}
on:close={() => dispatch('close')}
/>
{:else}
<div class="ac-header full divide caption-height withoutBackground">
<div class="ac-header__wrap-title mr-3">
{#if objectPresenter && object}
<Component is={objectPresenter.presenter} props={{ value: object }} />
{/if}
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="tool" on:click={() => dispatch('close')}>
<IconClose size="medium" />
</div>
</div>
{#if isLoading}
<div class="spinner">
<Spinner size="small" />
</div>
{/if}
{#if object}
<ActivityScrolledView
bind:isLoading
selectedMessageId={_id}
{object}
lastViewedTimestamp={notifyContext?.lastViewedTimestamp}
_class={hierarchy.isDerived(object._class, chunter.class.ChunterSpace)
? chunter.class.ChatMessage
: activity.class.ActivityMessage}
skipLabels={hierarchy.isDerived(object._class, chunter.class.ChunterSpace)}
/>
{/if}
{/if}
<style lang="scss">
.spinner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.tool {
margin-left: 0.75rem;
opacity: 0.4;
cursor: pointer;
&:hover {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,57 @@
<!--
// 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 { Scroller } from '@hcengineering/ui'
import { ActivityNotificationViewlet, DisplayInboxNotification } from '@hcengineering/notification'
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
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
async function handleCheck (notification: DisplayInboxNotification, isChecked: boolean) {
if (!isChecked) {
return
}
await deleteInboxNotification(notification)
}
</script>
<Scroller>
{#each notifications as notification (notification._id)}
<div animate:flip={{ duration: 500 }}>
<InboxNotificationPresenter
value={notification}
{viewlets}
onCheck={(isChecked) => handleCheck(notification, isChecked)}
onClick={() => {
dispatch('click', {
context: $notifyContextsStore.find(({ _id }) => _id === notification.docNotifyContext),
notification
})
}}
/>
</div>
{/each}
</Scroller>

View File

@ -0,0 +1,68 @@
<!--
// 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 { ActivityNotificationViewlet, DisplayInboxNotification, DocNotifyContext } from '@hcengineering/notification'
import { groupByArray, Ref } from '@hcengineering/core'
import { flip } from 'svelte/animate'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import DocNotifyContextCard from '../DocNotifyContextCard.svelte'
import { deleteContextNotifications } from '../../utils'
export let notifications: DisplayInboxNotification[] = []
export let viewlets: ActivityNotificationViewlet[] = []
const inboxClient = InboxNotificationsClientImpl.getClient()
const notifyContextsStore = inboxClient.docNotifyContexts
let displayNotificationsByContext = new Map<Ref<DocNotifyContext>, DisplayInboxNotification[]>()
$: displayNotificationsByContext = groupByArray(notifications, ({ docNotifyContext }) => docNotifyContext)
async function handleCheck (context: DocNotifyContext, isChecked: boolean) {
if (!isChecked) {
return
}
await deleteContextNotifications(context)
}
</script>
{#each displayNotificationsByContext as [contextId, contextNotifications] (contextId)}
<div animate:flip={{ duration: 500 }}>
{#if contextNotifications.length}
{@const context = $notifyContextsStore.find(({ _id }) => _id === contextId)}
{#if context}
<DocNotifyContextCard
value={context}
notifications={contextNotifications}
{viewlets}
on:click
on:check={(event) => handleCheck(context, event.detail)}
/>
<div class="separator" />
{/if}
{/if}
</div>
{/each}
<style lang="scss">
.separator {
width: 100%;
height: 1px;
background-color: var(--theme-navpanel-border);
}
</style>

View File

@ -17,9 +17,14 @@
import { getClient } from '@hcengineering/presentation'
import { Component } from '@hcengineering/ui'
import { Class, Doc, Ref } from '@hcengineering/core'
import { DisplayInboxNotification } from '@hcengineering/notification'
import { ActivityNotificationViewlet, DisplayInboxNotification } from '@hcengineering/notification'
export let value: DisplayInboxNotification
export let embedded = false
export let skipLabel = false
export let viewlets: ActivityNotificationViewlet[] = []
export let onClick: (() => void) | undefined = undefined
export let onCheck: ((isChecked: boolean) => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -28,5 +33,5 @@
</script>
{#if objectPresenter}
<Component is={objectPresenter.presenter} props={{ value }} />
<Component is={objectPresenter.presenter} props={{ value, embedded, skipLabel, viewlets, onClick, onCheck }} />
{/if}

View File

@ -182,21 +182,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
const notificationsToRead = this._inboxNotifications
.filter((n): n is ActivityInboxNotification => n._class === notification.class.ActivityInboxNotification)
.filter(({ attachedTo, attachedToClass, isViewed }) => {
return ids.includes(attachedTo) && !isViewed
// if (attachedToClass !== activity.class.DocUpdateMessage || $lookup?.attachedTo === undefined) {
// return false
// }
//
// const docUpdateMessage = $lookup.attachedTo as DocUpdateMessage
//
// if (docUpdateMessage.objectClass !== activity.class.Reaction) {
// return false
// }
//
// return ids.includes(docUpdateMessage.attachedTo as Ref<ActivityMessage>) && !isViewed
})
.filter(({ attachedTo, isViewed }) => ids.includes(attachedTo) && !isViewed)
await Promise.all(
notificationsToRead.map(async (notification) => await client.update(notification, { isViewed: true }))

View File

@ -21,11 +21,12 @@ import NotificationSettings from './components/NotificationSettings.svelte'
import NotificationPresenter from './components/NotificationPresenter.svelte'
import TxCollaboratorsChange from './components/activity/TxCollaboratorsChange.svelte'
import TxDmCreation from './components/activity/TxDmCreation.svelte'
import InboxAside from './components/inbox/InboxAside.svelte'
import DocNotifyContextPresenter from './components/DocNotifyContextPresenter.svelte'
import NotificationCollaboratorsChanged from './components/NotificationCollaboratorsChanged.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 {
unsubscribe,
resolveLocation,
@ -42,7 +43,12 @@ import {
pinDocNotifyContext,
unpinDocNotifyContext,
hideDocNotifyContext,
unHideDocNotifyContext
unHideDocNotifyContext,
canReadNotifyContext,
canUnReadNotifyContext,
readNotifyContext,
unReadNotifyContext,
deleteContextNotifications
} from './utils'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
@ -55,13 +61,14 @@ export { default as BrowserNotificatator } from './components/BrowserNotificatat
export default async (): Promise<Resources> => ({
component: {
Inbox,
InboxAside,
NotificationPresenter,
NotificationSettings,
NotificationCollaboratorsChanged,
DocNotifyContextPresenter,
ActivityInboxNotificationPresenter,
CommonInboxNotificationPresenter
CommonInboxNotificationPresenter,
InboxFlatListView,
InboxGroupedListView
},
activity: {
TxCollaboratorsChange,
@ -75,8 +82,10 @@ export default async (): Promise<Resources> => ({
HasDocNotifyContextPinAction: hasDocNotifyContextPinAction,
HasDocNotifyContextUnpinAction: hasDocNotifyContextUnpinAction,
IsDocNotifyContextHidden: isDocNotifyContextHidden,
IsDocNotifyContextVisible: isDocNotifyContextVisible,
HasHiddenDocNotifyContext: hasHiddenDocNotifyContext
IsDocNotifyContextTracked: isDocNotifyContextVisible,
HasHiddenDocNotifyContext: hasHiddenDocNotifyContext,
CanReadNotifyContext: canReadNotifyContext,
CanUnReadNotifyContext: canUnReadNotifyContext
},
actionImpl: {
Unsubscribe: unsubscribe,
@ -86,7 +95,10 @@ export default async (): Promise<Resources> => ({
PinDocNotifyContext: pinDocNotifyContext,
UnpinDocNotifyContext: unpinDocNotifyContext,
HideDocNotifyContext: hideDocNotifyContext,
UnHideDocNotifyContext: unHideDocNotifyContext
UnHideDocNotifyContext: unHideDocNotifyContext,
ReadNotifyContext: readNotifyContext,
UnReadNotifyContext: unReadNotifyContext,
DeleteContextNotifications: deleteContextNotifications
},
resolver: {
Location: resolveLocation

View File

@ -34,7 +34,6 @@ export default mergeIds(notificationId, notification, {
ChangeCollaborators: '' as IntlString,
Activity: '' as IntlString,
People: '' as IntlString,
All: '' as IntlString,
Read: '' as IntlString,
Unread: '' as IntlString
}

View File

@ -148,6 +148,70 @@ export async function isDocNotifyContextVisible (notifyContext: DocNotifyContext
return !notifyContext.hidden
}
/**
* @public
*/
export async function canReadNotifyContext (doc: DocNotifyContext): Promise<boolean> {
const inboxNotificationsClient = InboxNotificationsClientImpl.getClient()
return (
get(inboxNotificationsClient.inboxNotificationsByContext)
.get(doc._id)
?.some(({ isViewed }) => !isViewed) ?? false
)
}
/**
* @public
*/
export async function canUnReadNotifyContext (doc: DocNotifyContext): Promise<boolean> {
const inboxNotificationsClient = InboxNotificationsClientImpl.getClient()
return (
get(inboxNotificationsClient.inboxNotificationsByContext)
.get(doc._id)
?.every(({ isViewed }) => isViewed) ?? false
)
}
/**
* @public
*/
export async function readNotifyContext (doc: DocNotifyContext): Promise<void> {
const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
await inboxClient.readNotifications(inboxNotifications.map(({ _id }) => _id))
await client.update(doc, { lastViewedTimestamp: Date.now() })
}
/**
* @public
*/
export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void> {
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
if (inboxNotifications.length === 0) {
return
}
await inboxClient.unreadNotifications([inboxNotifications[0]._id])
}
/**
* @public
*/
export async function deleteContextNotifications (doc: DocNotifyContext): Promise<void> {
const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
await inboxClient.deleteNotifications(inboxNotifications.map(({ _id }) => _id))
await client.update(doc, { lastViewedTimestamp: Date.now() })
}
enum OpWithMe {
Add = 'add',
Remove = 'remove'
@ -232,74 +296,61 @@ export async function resolveLocation (loc: Location): Promise<ResolvedLocation
return undefined
}
const availableSpecies = ['all', 'reactions']
const special = loc.path[3]
const contextId = loc.fragment as Ref<DocNotifyContext> | undefined
if (!availableSpecies.includes(special)) {
if (contextId === undefined) {
return {
loc: {
path: [loc.path[0], loc.path[1], inboxId, 'all'],
path: [loc.path[0], loc.path[1], inboxId],
fragment: undefined
},
defaultLocation: {
path: [loc.path[0], loc.path[1], inboxId, 'all'],
path: [loc.path[0], loc.path[1], inboxId],
fragment: undefined
}
}
}
const _id = loc.path[4] as Ref<ActivityMessage> | undefined
if (_id !== undefined) {
return await generateLocation(loc, _id)
}
return await generateLocation(loc, contextId)
}
async function generateLocation (loc: Location, _id: Ref<ActivityMessage>): Promise<ResolvedLocation | undefined> {
async function generateLocation (
loc: Location,
contextId: Ref<DocNotifyContext>
): Promise<ResolvedLocation | undefined> {
const client = getClient()
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
const special = loc.path[3]
const messageId = loc.query?.message as Ref<ActivityMessage> | undefined
const availableSpecies = ['all', 'reactions']
const context = await client.findOne(notification.class.DocNotifyContext, { _id: contextId })
if (!availableSpecies.includes(special)) {
if (context === undefined) {
return {
loc: {
path: [appComponent, workspace, inboxId, 'all'],
path: [loc.path[0], loc.path[1], inboxId],
fragment: undefined
},
defaultLocation: {
path: [appComponent, workspace, inboxId, 'all'],
path: [loc.path[0], loc.path[1], inboxId],
fragment: undefined
}
}
}
const message = await client.findOne(activity.class.ActivityMessage, { _id })
if (message === undefined) {
return {
loc: {
path: [appComponent, workspace, inboxId, special],
fragment: undefined
},
defaultLocation: {
path: [appComponent, workspace, inboxId, special],
fragment: undefined
}
}
}
const message =
messageId !== undefined ? await client.findOne(activity.class.ActivityMessage, { _id: messageId }) : undefined
return {
loc: {
path: [appComponent, workspace, inboxId, special, _id],
fragment: undefined
path: [appComponent, workspace, inboxId],
fragment: contextId,
query: { message: message !== undefined ? (messageId as string) : null }
},
defaultLocation: {
path: [appComponent, workspace, inboxId, special, _id],
fragment: undefined
path: [appComponent, workspace, inboxId],
query: { message: message !== undefined ? (messageId as string) : null }
}
}
}

View File

@ -33,7 +33,7 @@ import { IntegrationType } from '@hcengineering/setting'
import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui'
import { Readable, Writable } from './types'
import { Preference } from '@hcengineering/preference'
import { Action } from '@hcengineering/view'
import { Action, Viewlet, ViewletDescriptor } from '@hcengineering/view'
import { ActivityMessage } from '@hcengineering/activity'
export * from './types'
@ -208,6 +208,13 @@ export interface NotificationPreview extends Class<Doc> {
presenter: AnyComponent
}
/**
* @public
*/
export interface NotificationContextPresenter extends Class<Doc> {
labelPresenter?: AnyComponent
}
/**
* @public
*/
@ -281,6 +288,14 @@ export interface InboxNotificationsClient {
*/
export type InboxNotificationsClientFactory = () => InboxNotificationsClient
/**
* @public
*/
export interface ActivityNotificationViewlet extends Doc {
messageMatch: DocumentQuery<Doc>
presenter: AnyComponent
}
/**
* @public
*/
@ -289,7 +304,8 @@ const notification = plugin(notificationId, {
ClassCollaborators: '' as Ref<Mixin<ClassCollaborators>>,
Collaborators: '' as Ref<Mixin<Collaborators>>,
NotificationObjectPresenter: '' as Ref<Mixin<NotificationObjectPresenter>>,
NotificationPreview: '' as Ref<Mixin<NotificationPreview>>
NotificationPreview: '' as Ref<Mixin<NotificationPreview>>,
NotificationContextPresenter: '' as Ref<Mixin<NotificationContextPresenter>>
},
class: {
Notification: '' as Ref<Class<Notification>>,
@ -302,7 +318,8 @@ const notification = plugin(notificationId, {
DocNotifyContext: '' as Ref<Class<DocNotifyContext>>,
InboxNotification: '' as Ref<Class<InboxNotification>>,
ActivityInboxNotification: '' as Ref<Class<ActivityInboxNotification>>,
CommonInboxNotification: '' as Ref<Class<CommonInboxNotification>>
CommonInboxNotification: '' as Ref<Class<CommonInboxNotification>>,
ActivityNotificationViewlet: '' as Ref<Class<ActivityNotificationViewlet>>
},
ids: {
NotificationSettings: '' as Ref<Doc>,
@ -321,11 +338,19 @@ const notification = plugin(notificationId, {
Inbox: '' as AnyComponent,
NotificationPresenter: '' as AnyComponent,
NotificationCollaboratorsChanged: '' as AnyComponent,
DocNotifyContextPresenter: '' as AnyComponent
DocNotifyContextPresenter: '' as AnyComponent,
InboxFlatListView: '' as AnyComponent,
InboxGroupedListView: '' 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>,
@ -333,7 +358,10 @@ const notification = plugin(notificationId, {
PinDocNotifyContext: '' as Ref<Action>,
UnpinDocNotifyContext: '' as Ref<Action>,
HideDocNotifyContext: '' as Ref<Action>,
UnHideDocNotifyContext: '' as Ref<Action>
UnHideDocNotifyContext: '' as Ref<Action>,
UnReadNotifyContext: '' as Ref<Action>,
ReadNotifyContext: '' as Ref<Action>,
DeleteContextNotifications: '' as Ref<Action>
},
icon: {
Notifications: '' as Asset,
@ -356,13 +384,16 @@ const notification = plugin(notificationId, {
NewCollaborators: '' as IntlString,
RemovedCollaborators: '' as IntlString,
Edited: '' as IntlString,
Pinned: '' as IntlString
Pinned: '' as IntlString,
FlatList: '' as IntlString,
GroupedList: '' as IntlString,
All: '' as IntlString
},
function: {
GetInboxNotificationsClient: '' as Resource<InboxNotificationsClientFactory>,
HasHiddenDocNotifyContext: '' as Resource<(doc: Doc[]) => Promise<boolean>>,
IsDocNotifyContextHidden: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
IsDocNotifyContextVisible: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
IsDocNotifyContextTracked: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>

View File

@ -82,12 +82,13 @@ import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svel
import MembersArrayEditor from './components/projects/MembersArrayEditor.svelte'
import {
getIssueId,
getIssueTitle,
issueIdentifierProvider,
issueIdProvider,
issueLinkFragmentProvider,
issueLinkProvider,
issueTitleProvider,
resolveLocation
getIssueTitle,
resolveLocation,
issueTitleProvider
} from './issues'
import tracker from './plugin'
@ -523,13 +524,14 @@ export default async (): Promise<Resources> => ({
await queryIssue(tracker.class.Issue, client, query, filter)
},
function: {
IssueTitleProvider: getIssueTitle,
IssueIdentifierProvider: issueIdentifierProvider,
IssueTitleProvider: issueTitleProvider,
ComponentTitleProvider: getComponentTitle,
MilestoneTitleProvider: getMilestoneTitle,
GetIssueId: issueIdProvider,
GetIssueLink: issueLinkProvider,
GetIssueLinkFragment: issueLinkFragmentProvider,
GetIssueTitle: issueTitleProvider,
GetIssueTitle: getIssueTitle,
IssueStatusSort: issueStatusSort,
IssuePrioritySort: issuePrioritySort,
MilestoneSort: milestoneSort,

View File

@ -18,7 +18,7 @@ export function isIssueId (shortLink: string): boolean {
return /^\S+-\d+$/.test(shortLink)
}
export async function getIssueTitle (client: TxOperations, ref: Ref<Doc>): Promise<string> {
export async function issueIdentifierProvider (client: TxOperations, ref: Ref<Doc>): Promise<string> {
const object = await client.findOne(
tracker.class.Issue,
{ _id: ref as Ref<Issue> },
@ -28,6 +28,20 @@ export async function getIssueTitle (client: TxOperations, ref: Ref<Doc>): Promi
return getIssueId(object.$lookup.space, object)
}
export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>): Promise<string> {
const object = await client.findOne(
tracker.class.Issue,
{ _id: ref as Ref<Issue> },
{ lookup: { space: tracker.class.Project } }
)
if (object === undefined) {
return ''
}
return await getIssueTitle(object)
}
async function getTitle (doc: Doc): Promise<string> {
const client = getClient()
const issue = doc as Issue
@ -55,7 +69,7 @@ export async function issueLinkFragmentProvider (doc: Doc): Promise<Location> {
return loc
}
export async function issueTitleProvider (doc: Issue): Promise<string> {
export async function getIssueTitle (doc: Issue): Promise<string> {
return await Promise.resolve(doc.title)
}

View File

@ -381,6 +381,7 @@ export default mergeIds(trackerId, tracker, {
},
function: {
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
IssueIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
ComponentTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
MilestoneTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
GetIssueId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,

View File

@ -0,0 +1,46 @@
<!--
// 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 { AttachedDoc, Class, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Component } from '@hcengineering/ui'
import view from '@hcengineering/view'
export let _id: Ref<AttachedDoc>
export let _class: Ref<Class<AttachedDoc>>
export let embedded: boolean = false
export let props = {}
const query = createQuery()
const client = getClient()
const hierarchy = client.getHierarchy()
let doc: AttachedDoc | undefined = undefined
$: query.query(_class, { _id }, (res) => {
doc = res[0]
})
$: panelMixin = doc ? hierarchy.classHierarchyMixin(doc.attachedToClass, view.mixin.ObjectPanel) : undefined
$: panelComponent = panelMixin?.component ?? view.component.EditDoc
</script>
{#if doc && panelComponent}
<Component
is={panelComponent}
props={{ embedded, _id: doc.attachedTo, _class: doc.attachedToClass, ...props }}
on:close
/>
{/if}

View File

@ -16,7 +16,7 @@
<script lang="ts">
import type { Doc, Ref } from '@hcengineering/core'
import type { Asset, IntlString } from '@hcengineering/platform'
import type { Action, AnySvelteComponent } from '@hcengineering/ui'
import type { Action, AnySvelteComponent, IconSize } from '@hcengineering/ui'
import {
ActionIcon,
Icon,
@ -34,6 +34,7 @@
export let _id: Ref<Doc> | string | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined
export let iconSize: IconSize = 'small'
export let label: IntlString | undefined = undefined
export let title: string | undefined = undefined
export let notifications = 0
@ -92,7 +93,7 @@
{/if}
{#if icon && !node}
<div class="an-element__icon" class:folder>
<Icon {icon} {iconProps} size={'small'} />
<Icon {icon} {iconProps} size={iconSize} />
</div>
{/if}
<span class="an-element__label" class:title={node} class:bold>

View File

@ -15,13 +15,14 @@
<script lang="ts">
import type { Doc, Ref } from '@hcengineering/core'
import type { Asset } from '@hcengineering/platform'
import type { Action } from '@hcengineering/ui'
import type { Action, IconSize } from '@hcengineering/ui'
import TreeElement from './TreeElement.svelte'
import { AnySvelteComponent } from '@hcengineering/ui'
export let _id: Ref<Doc>
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined
export let iconSize: IconSize = 'small'
export let title: string
export let notifications = 0
export let actions: (originalEvent?: MouseEvent) => Promise<Action[]> = async () => []
@ -34,6 +35,7 @@
<TreeElement
{_id}
{icon}
{iconSize}
{title}
{notifications}
{selected}

View File

@ -85,6 +85,7 @@ import DateFilterPresenter from './components/filter/DateFilterPresenter.svelte'
import ArrayFilter from './components/filter/ArrayFilter.svelte'
import SpaceHeader from './components/SpaceHeader.svelte'
import ViewletContentView from './components/ViewletContentView.svelte'
import AttachedDocPanel from './components/AttachedDocPanel.svelte'
import {
afterResult,
@ -251,7 +252,8 @@ export default async (): Promise<Resources> => ({
StatusPresenter,
StatusRefPresenter,
DateFilterPresenter,
StringFilterPresenter
StringFilterPresenter,
AttachedDocPanel
},
popup: {
PositionElementAlignment

View File

@ -998,7 +998,7 @@ export async function getDocTitle (
return await resource(client, objectId, object)
}
async function getDocIdentifier (
export async function getDocIdentifier (
client: Client,
objectId: Ref<Doc>,
objectClass: Ref<Class<Doc>>,

View File

@ -272,6 +272,13 @@ export interface ObjectTitle extends Class<Doc> {
titleProvider: Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>>
}
/**
* @public
*/
export interface ObjectIcon extends Class<Doc> {
component: AnyComponent
}
/**
* @public
*/
@ -807,7 +814,8 @@ const view = plugin(viewId, {
SpacePresenter: '' as Ref<Mixin<SpacePresenter>>,
AttributeFilterPresenter: '' as Ref<Mixin<AttributeFilterPresenter>>,
Aggregation: '' as Ref<Mixin<Aggregation>>,
Groupping: '' as Ref<Mixin<Groupping>>
Groupping: '' as Ref<Mixin<Groupping>>,
ObjectIcon: '' as Ref<Mixin<ObjectIcon>>
},
class: {
ViewletPreference: '' as Ref<Class<ViewletPreference>>,
@ -853,7 +861,8 @@ const view = plugin(viewId, {
ValueSelector: '' as AnyComponent,
GrowPresenter: '' as AnyComponent,
DividerPresenter: '' as AnyComponent,
IconWithEmoji: '' as AnyComponent
IconWithEmoji: '' as AnyComponent,
AttachedDocPanel: '' as AnyComponent
},
ids: {
IconWithEmoji: '' as Asset

View File

@ -19,9 +19,6 @@
import { NavLink } from '@hcengineering/view-resources'
import type { Application } from '@hcengineering/workbench'
import workbench from '@hcengineering/workbench'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { DocNotifyContext } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import AppItem from './AppItem.svelte'
import preference from '@hcengineering/preference'
@ -47,19 +44,6 @@
$: filteredApps = apps.filter((it) => !hiddenAppsIds.includes(it._id))
$: topApps = filteredApps.filter((it) => it.position === 'top')
$: bottomdApps = filteredApps.filter((it) => it.position !== 'top')
const inboxClient = InboxNotificationsClientImpl.getClient()
const docNotifyContextsStore = inboxClient.docNotifyContexts
async function shouldNotify (app: Application, docNotifyContexts: DocNotifyContext[]) {
if (!app.shouldNotify) {
return false
}
const shouldNotifyFn = await getResource(app.shouldNotify)
return await shouldNotifyFn(docNotifyContexts)
}
</script>
<div class="flex-{direction === 'horizontal' ? 'row-center' : 'col-center'} clear-mins apps-{direction} relative">
@ -74,17 +58,13 @@
>
{#each topApps as app}
<NavLink app={app.alias} shrink={0}>
{#await shouldNotify(app, $docNotifyContextsStore) then notify}
<AppItem selected={app._id === active} icon={app.icon} label={app.label} {notify} />
{/await}
<AppItem selected={app._id === active} icon={app.icon} label={app.label} />
</NavLink>
{/each}
<div class="divider" />
{#each bottomdApps as app}
<NavLink app={app.alias} shrink={0}>
{#await shouldNotify(app, $docNotifyContextsStore) then notify}
<AppItem selected={app._id === active} icon={app.icon} label={app.label} {notify} />
{/await}
<AppItem selected={app._id === active} icon={app.icon} label={app.label} />
</NavLink>
{/each}
<div class="apps-space-{direction}" />

View File

@ -14,7 +14,6 @@
//
import type { Class, Doc, Mixin, Obj, Ref, Space } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification'
import type { Asset, IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
@ -44,7 +43,6 @@ export interface Application extends Doc {
checkIsHeaderHidden?: Resource<() => Promise<boolean>>
checkIsHeaderDisabled?: Resource<() => Promise<boolean>>
navFooterComponent?: AnyComponent
shouldNotify?: Resource<(docNotifyContexts: DocNotifyContext[]) => Promise<boolean>>
}
/**