mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-4361: update inbox ui (#4376)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
12924cb79c
commit
13a3914a66
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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>>
|
||||
|
@ -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 }
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
64
models/notification/src/viewlets.ts
Normal file
64
models/notification/src/viewlets.ts
Normal 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
|
||||
)
|
||||
}
|
@ -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, {
|
||||
|
@ -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(
|
||||
|
@ -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 |
@ -31,6 +31,7 @@
|
||||
"Unset": "Unset",
|
||||
"Update": "Update",
|
||||
"Updated": "Updated",
|
||||
"UpdatedCollection": "Updated"
|
||||
"UpdatedCollection": "Updated",
|
||||
"Message": "Message"
|
||||
}
|
||||
}
|
@ -31,6 +31,7 @@
|
||||
"Unset": "Cбросил",
|
||||
"Update": "Обновить",
|
||||
"Updated": "Обновил(а)",
|
||||
"UpdatedCollection": "Обновленные"
|
||||
"UpdatedCollection": "Обновленные",
|
||||
"Message": "Сообщение"
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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>
|
@ -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
|
||||
}}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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}"
|
||||
}
|
||||
}
|
@ -83,7 +83,8 @@
|
||||
"NewestFirst": "Сначала новые",
|
||||
"ReplyToThread": "Ответить в канале",
|
||||
"SentMessage": "Отправил(а) сообщение",
|
||||
"Direct": "личные сообщения",
|
||||
"RepliedToThread": "Ответил(а) в канале"
|
||||
"Direct": "Личные сообщения",
|
||||
"RepliedToThread": "Ответил(а) в канале",
|
||||
"RepliedTo": "Ответил(а) на: {message}"
|
||||
}
|
||||
}
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
28
plugins/chunter-resources/src/components/ChannelIcon.svelte
Normal file
28
plugins/chunter-resources/src/components/ChannelIcon.svelte
Normal 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}
|
51
plugins/chunter-resources/src/components/ChannelPanel.svelte
Normal file
51
plugins/chunter-resources/src/components/ChannelPanel.svelte
Normal 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}
|
@ -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) => {
|
||||
|
@ -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}
|
95
plugins/chunter-resources/src/components/DirectIcon.svelte
Normal file
95
plugins/chunter-resources/src/components/DirectIcon.svelte
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
@ -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} />
|
@ -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}
|
||||
/>
|
||||
|
@ -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[]) => {
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
}
|
||||
})
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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} />
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -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>,
|
||||
|
@ -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 |
@ -33,6 +33,8 @@
|
||||
"RemovedCollaborators": "Removed collaborators",
|
||||
"Edited": "edited",
|
||||
"Pinned": "Pinned",
|
||||
"Message": "Message"
|
||||
"Message": "Message",
|
||||
"FlatList": "Flat list",
|
||||
"GroupedList": "Grouped list"
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,8 @@
|
||||
"RemovedCollaborators": "Удалены участники",
|
||||
"Edited": "отредактировал(а)",
|
||||
"Pinned": "Закреплено",
|
||||
"Message": "Сообщение"
|
||||
"Message": "Сообщение",
|
||||
"FlatList": "Flat list",
|
||||
"GroupedList": "Grouped list"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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}
|
||||
|
@ -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 }))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>>
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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>>,
|
||||
|
@ -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}
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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>>,
|
||||
|
@ -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
|
||||
|
@ -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}" />
|
||||
|
@ -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>>
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user