mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-4360: rewrite chat (#4265)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
06f6f6a222
commit
7f6f5e28a6
@ -27,11 +27,15 @@
|
||||
"dependencies": {
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/activity-resources": "^0.6.1",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/model": "^0.6.7",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/model-preference": "^0.6.0",
|
||||
"@hcengineering/model-view": "^0.6.0",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/model-view": "^0.6.0"
|
||||
"@hcengineering/view": "^0.6.9"
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
type Reaction,
|
||||
type TxViewlet,
|
||||
type ActivityMessageControl,
|
||||
type SavedMessage,
|
||||
type IgnoreActivity
|
||||
} from '@hcengineering/activity'
|
||||
import core, {
|
||||
@ -43,7 +44,8 @@ import core, {
|
||||
IndexKind,
|
||||
type TxCUD,
|
||||
type Domain,
|
||||
type Account
|
||||
type Account,
|
||||
type Timestamp
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
Model,
|
||||
@ -55,11 +57,16 @@ import {
|
||||
Mixin,
|
||||
Collection,
|
||||
TypeBoolean,
|
||||
TypeIntlString
|
||||
TypeIntlString,
|
||||
ArrOf,
|
||||
TypeTimestamp
|
||||
} from '@hcengineering/model'
|
||||
import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
|
||||
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import contact, { type Person } from '@hcengineering/contact'
|
||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||
import notification from '@hcengineering/notification'
|
||||
import view from '@hcengineering/model-view'
|
||||
|
||||
import activity from './plugin'
|
||||
@ -102,8 +109,18 @@ export class TActivityMessage extends TAttachedDoc implements ActivityMessage {
|
||||
@Prop(TypeBoolean(), activity.string.Pinned)
|
||||
isPinned?: boolean
|
||||
|
||||
@Prop(ArrOf(TypeRef(contact.class.Person)), contact.string.Person)
|
||||
repliedPersons?: Ref<Person>[]
|
||||
|
||||
@Prop(TypeTimestamp(), activity.string.LastReply)
|
||||
@Index(IndexKind.Indexed)
|
||||
lastReply?: Timestamp
|
||||
|
||||
@Prop(Collection(activity.class.Reaction), activity.string.Reactions)
|
||||
reactions?: number
|
||||
|
||||
@Prop(Collection(activity.class.ActivityMessage), activity.string.Replies)
|
||||
replies?: number
|
||||
}
|
||||
|
||||
@Model(activity.class.DocUpdateMessage, activity.class.ActivityMessage)
|
||||
@ -186,11 +203,20 @@ export class TActivityExtension extends TDoc implements ActivityExtension {
|
||||
@Model(activity.class.ActivityMessagesFilter, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TActivityMessagesFilter extends TDoc implements ActivityMessagesFilter {
|
||||
label!: IntlString
|
||||
position!: number
|
||||
filter!: Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||
}
|
||||
|
||||
@Model(activity.class.Reaction, core.class.AttachedDoc, DOMAIN_ACTIVITY)
|
||||
export class TReaction extends TAttachedDoc implements Reaction {
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedTo)
|
||||
@Index(IndexKind.Indexed)
|
||||
declare attachedTo: Ref<ActivityMessage>
|
||||
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedToClass)
|
||||
@Index(IndexKind.Indexed)
|
||||
declare attachedToClass: Ref<Class<ActivityMessage>>
|
||||
|
||||
@Prop(TypeString(), activity.string.Emoji)
|
||||
emoji!: string
|
||||
|
||||
@ -198,6 +224,11 @@ export class TReaction extends TAttachedDoc implements Reaction {
|
||||
createBy!: Ref<Account>
|
||||
}
|
||||
|
||||
@Model(activity.class.SavedMessage, preference.class.Preference)
|
||||
export class TSavedMessage extends TPreference implements SavedMessage {
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), view.string.Save)
|
||||
declare attachedTo: Ref<ActivityMessage>
|
||||
}
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TTxViewlet,
|
||||
@ -212,6 +243,7 @@ export function createModel (builder: Builder): void {
|
||||
TActivityAttributeUpdatesPresenter,
|
||||
TActivityInfoMessage,
|
||||
TActivityMessageControl,
|
||||
TSavedMessage,
|
||||
TIgnoreActivity
|
||||
)
|
||||
|
||||
@ -225,13 +257,30 @@ export function createModel (builder: Builder): void {
|
||||
presenter: activity.component.ActivityInfoMessagePresenter
|
||||
})
|
||||
|
||||
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, view.mixin.LinkProvider, {
|
||||
encode: activity.function.GetFragment
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.ActivityMessagesFilter,
|
||||
core.space.Model,
|
||||
{
|
||||
label: activity.string.All,
|
||||
position: 10,
|
||||
filter: activity.filter.AllFilter
|
||||
},
|
||||
activity.ids.AllFilter
|
||||
)
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
|
||||
label: activity.string.Attributes,
|
||||
position: 10,
|
||||
filter: activity.filter.AttributesFilter
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
|
||||
label: activity.string.Pinned,
|
||||
position: 20,
|
||||
filter: activity.filter.PinnedFilter
|
||||
})
|
||||
|
||||
@ -259,6 +308,31 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
activity.ids.ReactionRemovedActivityViewlet
|
||||
)
|
||||
|
||||
builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.ClassCollaborators, {
|
||||
fields: ['createdBy', 'repliedPersons']
|
||||
})
|
||||
|
||||
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, notification.mixin.ClassCollaborators, {
|
||||
fields: ['createdBy', 'repliedPersons']
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
{
|
||||
hidden: false,
|
||||
generated: false,
|
||||
label: activity.string.Reactions,
|
||||
group: activity.ids.ActivityNotificationGroup,
|
||||
txClasses: [core.class.TxCreateDoc],
|
||||
objectClass: activity.class.Reaction,
|
||||
providers: {
|
||||
[notification.providers.PlatformNotification]: true
|
||||
}
|
||||
},
|
||||
activity.ids.AddReactionNotification
|
||||
)
|
||||
}
|
||||
|
||||
export default activity
|
||||
|
@ -16,20 +16,33 @@ import { activityId, type ActivityMessage, type DocUpdateMessageViewlet } from '
|
||||
import activity from '@hcengineering/activity-resources/src/plugin'
|
||||
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
||||
import { type Doc, type Ref } from '@hcengineering/core'
|
||||
import type { Location } from '@hcengineering/ui'
|
||||
import { type ActionCategory } from '@hcengineering/view'
|
||||
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
|
||||
|
||||
export default mergeIds(activityId, activity, {
|
||||
string: {
|
||||
Attributes: '' as IntlString,
|
||||
Pinned: '' as IntlString,
|
||||
Emoji: '' as IntlString,
|
||||
Reacted: '' as IntlString
|
||||
Reacted: '' as IntlString,
|
||||
Replies: '' as IntlString
|
||||
},
|
||||
filter: {
|
||||
AttributesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
|
||||
PinnedFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||
PinnedFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
|
||||
AllFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||
},
|
||||
ids: {
|
||||
ReactionAddedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
ReactionRemovedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>
|
||||
ReactionRemovedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
ActivityNotificationGroup: '' as Ref<NotificationGroup>,
|
||||
AddReactionNotification: '' as Ref<NotificationType>
|
||||
},
|
||||
function: {
|
||||
GetFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
|
||||
},
|
||||
category: {
|
||||
Activity: '' as Ref<ActionCategory>
|
||||
}
|
||||
})
|
||||
|
@ -55,12 +55,13 @@ export const migrateOperations: [string, MigrateOperation][] = [
|
||||
['view', viewOperation],
|
||||
['contact', contactOperation],
|
||||
['tags', tagsOperation],
|
||||
['notification', notificationOperation],
|
||||
['setting', settingOperation],
|
||||
['tracker', trackerOperation],
|
||||
['board', boardOperation],
|
||||
['hr', hrOperation],
|
||||
['bitrix', bitrixOperation],
|
||||
['inventiry', inventoryOperation],
|
||||
['activityServer', activityServerOperation]
|
||||
['activityServer', activityServerOperation],
|
||||
// We should call it after activityServer and chunter
|
||||
['notification', notificationOperation]
|
||||
]
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
import core, { TAttachedDoc } from '@hcengineering/model-core'
|
||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||
import view, { createAction } from '@hcengineering/model-view'
|
||||
|
||||
import attachment from './plugin'
|
||||
|
||||
export { attachmentId } from '@hcengineering/attachment'
|
||||
@ -150,6 +151,7 @@ export function createModel (builder: Builder): void {
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
|
||||
label: attachment.string.FilterAttachments,
|
||||
position: 50,
|
||||
filter: attachment.filter.AttachmentsFilter
|
||||
})
|
||||
|
||||
|
@ -24,8 +24,7 @@ import type { ActionCategory } from '@hcengineering/view'
|
||||
|
||||
export default mergeIds(attachmentId, attachment, {
|
||||
component: {
|
||||
AttachmentPresenter: '' as AnyComponent,
|
||||
FileBrowser: '' as AnyComponent
|
||||
AttachmentPresenter: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
AddAttachment: '' as IntlString,
|
||||
|
@ -36,7 +36,6 @@
|
||||
"@hcengineering/model-activity": "^0.6.0",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/model-notification": "^0.6.0",
|
||||
"@hcengineering/model-preference": "^0.6.0",
|
||||
"@hcengineering/model-view": "^0.6.0",
|
||||
"@hcengineering/model-workbench": "^0.6.1",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
|
@ -13,22 +13,22 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import activity from '@hcengineering/activity'
|
||||
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
||||
import {
|
||||
type Backlink,
|
||||
type Channel,
|
||||
chunterId,
|
||||
type ChunterMessage,
|
||||
type ChunterMessageExtension,
|
||||
type ChunterSpace,
|
||||
type Comment,
|
||||
type DirectMessage,
|
||||
type Message,
|
||||
type SavedMessages,
|
||||
type ThreadMessage,
|
||||
type DirectMessageInput,
|
||||
type ChatMessage,
|
||||
type ChatMessageViewlet
|
||||
type ChatMessageViewlet,
|
||||
type ChunterSpace,
|
||||
type ObjectChatPanel,
|
||||
type ThreadMessage
|
||||
} from '@hcengineering/chunter'
|
||||
import contact, { type Person } from '@hcengineering/contact'
|
||||
import {
|
||||
@ -61,14 +61,14 @@ import {
|
||||
import attachment from '@hcengineering/model-attachment'
|
||||
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
|
||||
import notification from '@hcengineering/model-notification'
|
||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||
import view, { createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view'
|
||||
import workbench from '@hcengineering/model-workbench'
|
||||
import chunter from './plugin'
|
||||
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import { TypeBoolean } from '@hcengineering/model'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import type { IntlString, Resource } from '@hcengineering/platform'
|
||||
import { TActivityMessage } from '@hcengineering/model-activity'
|
||||
|
||||
export { chunterId } from '@hcengineering/chunter'
|
||||
export { chunterOperation } from './migration'
|
||||
|
||||
@ -119,14 +119,6 @@ export class TChunterMessage extends TAttachedDoc implements ChunterMessage {
|
||||
@Mixin(chunter.mixin.ChunterMessageExtension, chunter.class.ChunterMessage)
|
||||
export class TChunterMessageExtension extends TChunterMessage implements ChunterMessageExtension {}
|
||||
|
||||
@Model(chunter.class.ThreadMessage, chunter.class.ChunterMessage)
|
||||
@UX(chunter.string.ThreadMessage, undefined, 'TMSG')
|
||||
export class TThreadMessage extends TChunterMessage implements ThreadMessage {
|
||||
declare attachedTo: Ref<Message>
|
||||
|
||||
declare attachedToClass: Ref<Class<Message>>
|
||||
}
|
||||
|
||||
@Model(chunter.class.Message, chunter.class.ChunterMessage)
|
||||
@UX(chunter.string.Message, undefined, 'MSG')
|
||||
export class TMessage extends TChunterMessage implements Message {
|
||||
@ -139,7 +131,7 @@ export class TMessage extends TChunterMessage implements Message {
|
||||
|
||||
repliesCount?: number
|
||||
|
||||
@Prop(TypeTimestamp(), chunter.string.LastReply)
|
||||
@Prop(TypeTimestamp(), activity.string.LastReply)
|
||||
lastReply?: Timestamp
|
||||
}
|
||||
|
||||
@ -167,12 +159,6 @@ export class TBacklink extends TComment implements Backlink {
|
||||
backlinkClass!: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
@Model(chunter.class.SavedMessages, preference.class.Preference)
|
||||
export class TSavedMessages extends TPreference implements SavedMessages {
|
||||
@Prop(TypeRef(chunter.class.ChunterMessage), chunter.string.SavedMessages)
|
||||
declare attachedTo: Ref<ChunterMessage>
|
||||
}
|
||||
|
||||
@Mixin(chunter.mixin.DirectMessageInput, core.class.Class)
|
||||
export class TDirectMessageInput extends TClass implements DirectMessageInput {
|
||||
component!: AnyComponent
|
||||
@ -184,13 +170,31 @@ export class TChatMessage extends TActivityMessage implements ChatMessage {
|
||||
@Index(IndexKind.FullText)
|
||||
message!: string
|
||||
|
||||
@Prop(TypeTimestamp(), chunter.string.Edit)
|
||||
@Index(IndexKind.Indexed)
|
||||
editedOn?: Timestamp
|
||||
|
||||
@Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, {
|
||||
shortLabel: attachment.string.Files
|
||||
})
|
||||
attachments?: number
|
||||
}
|
||||
|
||||
@Prop(TypeBoolean(), core.string.Boolean)
|
||||
isEdited?: boolean
|
||||
@Model(chunter.class.ThreadMessage, chunter.class.ChatMessage)
|
||||
export class TThreadMessage extends TChatMessage implements ThreadMessage {
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedTo)
|
||||
declare attachedTo: Ref<ActivityMessage>
|
||||
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedToClass)
|
||||
declare attachedToClass: Ref<Class<ActivityMessage>>
|
||||
|
||||
@Prop(TypeRef(core.class.Doc), core.string.Object)
|
||||
@Index(IndexKind.Indexed)
|
||||
objectId!: Ref<Doc>
|
||||
|
||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
||||
@Index(IndexKind.Indexed)
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
@Model(chunter.class.ChatMessageViewlet, core.class.Doc, DOMAIN_MODEL)
|
||||
@ -199,31 +203,45 @@ export class TChatMessageViewlet extends TDoc implements ChatMessageViewlet {
|
||||
@Index(IndexKind.Indexed)
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
|
||||
@Prop(TypeRef(core.class.Doc), core.string.Class)
|
||||
@Index(IndexKind.Indexed)
|
||||
messageClass!: Ref<Class<Doc>>
|
||||
|
||||
label?: IntlString
|
||||
onlyWithParent?: boolean
|
||||
}
|
||||
|
||||
@Mixin(chunter.mixin.ObjectChatPanel, core.class.Class)
|
||||
export class TObjectChatPanel extends TClass implements ObjectChatPanel {
|
||||
ignoreKeys!: string[]
|
||||
titleProvider!: Resource<(object: Doc) => string>
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder, options = { addApplication: true }): void {
|
||||
builder.createModel(
|
||||
TChunterSpace,
|
||||
TChannel,
|
||||
TMessage,
|
||||
TThreadMessage,
|
||||
TChunterMessage,
|
||||
TChunterMessageExtension,
|
||||
TComment,
|
||||
TBacklink,
|
||||
TDirectMessage,
|
||||
TSavedMessages,
|
||||
TDirectMessageInput,
|
||||
TChatMessage,
|
||||
TChatMessageViewlet
|
||||
TThreadMessage,
|
||||
TChatMessageViewlet,
|
||||
TObjectChatPanel
|
||||
)
|
||||
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
|
||||
|
||||
spaceClasses.forEach((spaceClass) => {
|
||||
builder.mixin(spaceClass, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
||||
builder.mixin(spaceClass, core.class.Class, view.mixin.LinkProvider, {
|
||||
encode: chunter.function.GetChunterSpaceLinkFragment
|
||||
})
|
||||
|
||||
builder.mixin(spaceClass, core.class.Class, workbench.mixin.SpaceView, {
|
||||
view: {
|
||||
class: chunter.class.Message
|
||||
@ -239,14 +257,14 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
})
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.SpaceName, {
|
||||
getName: chunter.function.GetDmName
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Message, core.class.Class, notification.mixin.ClassCollaborators, {
|
||||
fields: ['createdBy', 'replies']
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectTitle, {
|
||||
titleProvider: chunter.function.DirectMessageTitleProvider
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.ClassCollaborators, {
|
||||
fields: ['members']
|
||||
})
|
||||
@ -279,10 +297,6 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
presenter: chunter.component.ThreadParentPresenter
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: chunter.component.MessagePresenter
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Message, core.class.Class, view.mixin.ObjectPanel, {
|
||||
component: chunter.component.ThreadViewPanel
|
||||
})
|
||||
@ -303,23 +317,6 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
header: chunter.component.ChannelHeader
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ViewletDescriptor,
|
||||
core.space.Model,
|
||||
{
|
||||
label: chunter.string.Chat,
|
||||
icon: view.icon.Table,
|
||||
component: chunter.component.ChannelView
|
||||
},
|
||||
chunter.viewlet.Chat
|
||||
)
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: chunter.class.Message,
|
||||
descriptor: chunter.viewlet.Chat,
|
||||
config: []
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ActionCategory,
|
||||
core.space.Model,
|
||||
@ -327,38 +324,6 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
chunter.category.Chunter
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: chunter.actionImpl.MarkUnread,
|
||||
label: chunter.string.MarkUnread,
|
||||
input: 'focus',
|
||||
category: chunter.category.Chunter,
|
||||
target: chunter.class.Message,
|
||||
context: {
|
||||
mode: 'context',
|
||||
group: 'edit'
|
||||
}
|
||||
},
|
||||
chunter.action.MarkUnread
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
label: chunter.string.MarkUnread,
|
||||
action: chunter.actionImpl.MarkCommentUnread,
|
||||
input: 'focus',
|
||||
category: chunter.category.Chunter,
|
||||
target: chunter.class.ThreadMessage,
|
||||
context: {
|
||||
mode: 'context',
|
||||
group: 'edit'
|
||||
}
|
||||
},
|
||||
chunter.action.MarkCommentUnread
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
@ -423,164 +388,20 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
{
|
||||
label: chunter.string.ApplicationLabelChunter,
|
||||
icon: chunter.icon.Chunter,
|
||||
locationResolver: chunter.resolver.Location,
|
||||
alias: chunterId,
|
||||
hidden: false,
|
||||
navigatorModel: {
|
||||
specials: [
|
||||
{
|
||||
id: 'spaceBrowser',
|
||||
component: workbench.component.SpaceBrowser,
|
||||
icon: chunter.icon.ChannelBrowser,
|
||||
label: chunter.string.ChannelBrowser,
|
||||
position: 'top',
|
||||
spaceClass: chunter.class.Channel,
|
||||
componentProps: {
|
||||
_class: chunter.class.Channel,
|
||||
label: chunter.string.ChannelBrowser,
|
||||
createItemDialog: chunter.component.CreateChannel,
|
||||
createItemLabel: chunter.string.CreateChannel
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'archive',
|
||||
component: workbench.component.Archive,
|
||||
icon: view.icon.Archive,
|
||||
label: workbench.string.Archive,
|
||||
position: 'top',
|
||||
visibleIf: workbench.function.HasArchiveSpaces,
|
||||
spaceClass: chunter.class.Channel
|
||||
},
|
||||
{
|
||||
id: 'threads',
|
||||
label: chunter.string.Threads,
|
||||
icon: chunter.icon.Thread,
|
||||
component: chunter.component.Threads,
|
||||
position: 'top'
|
||||
},
|
||||
{
|
||||
id: 'savedItems',
|
||||
label: chunter.string.SavedItems,
|
||||
icon: chunter.icon.Bookmark,
|
||||
component: chunter.component.SavedMessages
|
||||
},
|
||||
{
|
||||
id: 'fileBrowser',
|
||||
label: attachment.string.FileBrowser,
|
||||
icon: attachment.icon.FileBrowser,
|
||||
component: attachment.component.FileBrowser,
|
||||
componentProps: {
|
||||
requestedSpaceClasses: [chunter.class.Channel, chunter.class.DirectMessage]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'chunterBrowser',
|
||||
label: chunter.string.ChunterBrowser,
|
||||
icon: workbench.icon.Search,
|
||||
component: chunter.component.ChunterBrowser,
|
||||
visibleIf: chunter.function.ChunterBrowserVisible
|
||||
}
|
||||
],
|
||||
spaces: [
|
||||
{
|
||||
id: 'channels',
|
||||
label: chunter.string.Channels,
|
||||
spaceClass: chunter.class.Channel,
|
||||
addSpaceLabel: chunter.string.CreateChannel,
|
||||
createComponent: chunter.component.CreateChannel
|
||||
},
|
||||
{
|
||||
id: 'directMessages',
|
||||
label: chunter.string.DirectMessages,
|
||||
spaceClass: chunter.class.DirectMessage,
|
||||
addSpaceLabel: chunter.string.NewDirectMessage,
|
||||
createComponent: chunter.component.CreateDirectMessage
|
||||
}
|
||||
],
|
||||
aside: chunter.component.ThreadView
|
||||
}
|
||||
component: chunter.component.Chat,
|
||||
aside: chunter.component.ThreadView,
|
||||
shouldNotify: chunter.function.ShouldNotify
|
||||
},
|
||||
chunter.app.Chunter
|
||||
)
|
||||
}
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.TxViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.Comment,
|
||||
icon: chunter.icon.Chunter,
|
||||
txClass: core.class.TxCreateDoc,
|
||||
component: chunter.activity.TxCommentCreate,
|
||||
label: chunter.string.LeftComment,
|
||||
display: 'content',
|
||||
editable: true,
|
||||
hideOnRemove: true
|
||||
},
|
||||
chunter.ids.TxCommentCreate
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.TxViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.ChatMessage,
|
||||
icon: chunter.icon.Chunter,
|
||||
txClass: core.class.TxCreateDoc,
|
||||
component: chunter.activity.TxCommentCreate,
|
||||
label: chunter.string.LeftComment,
|
||||
display: 'content',
|
||||
editable: true,
|
||||
hideOnRemove: true
|
||||
},
|
||||
chunter.ids.TxChatMessageCreate
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: view.actionImpl.CopyTextToClipboard,
|
||||
actionProps: {
|
||||
textProvider: chunter.function.GetLink
|
||||
},
|
||||
label: chunter.string.CopyLink,
|
||||
icon: chunter.icon.Thread,
|
||||
keyBinding: [],
|
||||
input: 'none',
|
||||
category: chunter.category.Chunter,
|
||||
target: chunter.class.Comment,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'copy'
|
||||
}
|
||||
},
|
||||
chunter.action.CopyCommentLink
|
||||
)
|
||||
|
||||
builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [view.action.Open]
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Message, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [view.action.Open]
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [view.action.Open]
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.LinkProvider, {
|
||||
encode: chunter.function.GetFragment
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.LinkProvider, {
|
||||
encode: chunter.function.GetFragment
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Message, core.class.Class, view.mixin.LinkProvider, {
|
||||
encode: chunter.function.GetFragment
|
||||
})
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
@ -593,114 +414,19 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
keyBinding: [],
|
||||
input: 'none',
|
||||
category: chunter.category.Chunter,
|
||||
target: chunter.class.Message,
|
||||
target: activity.class.ActivityMessage,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
application: chunter.app.Chunter,
|
||||
group: 'copy'
|
||||
}
|
||||
},
|
||||
chunter.action.CopyMessageLink
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: view.actionImpl.CopyTextToClipboard,
|
||||
actionProps: {
|
||||
textProvider: chunter.function.GetLink
|
||||
},
|
||||
label: chunter.string.CopyLink,
|
||||
icon: chunter.icon.Thread,
|
||||
keyBinding: [],
|
||||
input: 'none',
|
||||
category: chunter.category.Chunter,
|
||||
target: chunter.class.ThreadMessage,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
application: chunter.app.Chunter,
|
||||
group: 'copy'
|
||||
}
|
||||
},
|
||||
chunter.action.CopyThreadMessageLink
|
||||
)
|
||||
|
||||
// We need to define this one, to hide default attached object removed case
|
||||
builder.createDoc(
|
||||
activity.class.TxViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.Comment,
|
||||
icon: chunter.icon.Chunter,
|
||||
txClass: core.class.TxRemoveDoc,
|
||||
display: 'inline',
|
||||
hideOnRemove: true
|
||||
},
|
||||
chunter.ids.TxCommentRemove
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.TxViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.ChatMessage,
|
||||
icon: chunter.icon.Chunter,
|
||||
txClass: core.class.TxRemoveDoc,
|
||||
display: 'inline',
|
||||
hideOnRemove: true
|
||||
},
|
||||
chunter.ids.TxChatMessageRemove
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.TxViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.Message,
|
||||
icon: chunter.icon.Chunter,
|
||||
txClass: core.class.TxCreateDoc,
|
||||
component: chunter.activity.TxMessageCreate,
|
||||
label: chunter.string.DMNotification,
|
||||
display: 'content',
|
||||
editable: true,
|
||||
hideOnRemove: true
|
||||
},
|
||||
chunter.ids.TxMessageCreate
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.TxViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.Backlink,
|
||||
icon: chunter.icon.Chunter,
|
||||
txClass: core.class.TxCreateDoc,
|
||||
component: chunter.component.BacklinkContent,
|
||||
label: chunter.string.MentionedIn,
|
||||
labelComponent: chunter.component.BacklinkReference,
|
||||
display: 'emphasized',
|
||||
editable: false,
|
||||
hideOnRemove: true
|
||||
},
|
||||
chunter.ids.TxBacklinkCreate
|
||||
)
|
||||
|
||||
// We need to define this one, to hide default attached object removed case
|
||||
builder.createDoc(
|
||||
activity.class.TxViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.Backlink,
|
||||
icon: chunter.icon.Chunter,
|
||||
txClass: core.class.TxRemoveDoc,
|
||||
display: 'inline',
|
||||
hideOnRemove: true
|
||||
},
|
||||
chunter.ids.TxBacklinkRemove
|
||||
chunter.action.CopyChatMessageLink
|
||||
)
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
|
||||
label: chunter.string.FilterBacklinks,
|
||||
position: 60,
|
||||
filter: chunter.filter.BacklinksFilter
|
||||
})
|
||||
|
||||
@ -753,7 +479,7 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
generated: false,
|
||||
hidden: false,
|
||||
txClasses: [core.class.TxCreateDoc],
|
||||
objectClass: chunter.class.Message,
|
||||
objectClass: chunter.class.ChatMessage,
|
||||
providers: {
|
||||
[notification.providers.EmailNotification]: false,
|
||||
[notification.providers.PlatformNotification]: true
|
||||
@ -776,7 +502,7 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
generated: false,
|
||||
hidden: false,
|
||||
txClasses: [core.class.TxCreateDoc],
|
||||
objectClass: chunter.class.Message,
|
||||
objectClass: chunter.class.ChatMessage,
|
||||
providers: {
|
||||
[notification.providers.PlatformNotification]: true
|
||||
},
|
||||
@ -841,10 +567,16 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
|
||||
label: chunter.string.Comments,
|
||||
position: 60,
|
||||
filter: chunter.filter.ChatMessagesFilter
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ChatMessage, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
||||
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectIdentifier, {
|
||||
provider: chunter.function.DmIdentifierProvider
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ChatMessage, core.class.Class, view.mixin.CollectionPresenter, {
|
||||
presenter: chunter.component.ChatMessagesPresenter
|
||||
})
|
||||
@ -853,6 +585,21 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
presenter: chunter.component.ChatMessagePresenter
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: chunter.component.ThreadMessagePresenter
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ThreadMessage,
|
||||
objectClass: chunter.class.ChatMessage,
|
||||
label: chunter.string.RepliedToThread
|
||||
},
|
||||
chunter.ids.ThreadMessageViewlet
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
@ -863,10 +610,59 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
keyBinding: ['Backspace'],
|
||||
category: chunter.category.Chunter,
|
||||
target: chunter.class.ChatMessage,
|
||||
context: { mode: ['context', 'browser'], group: 'edit' }
|
||||
visibilityTester: chunter.function.CanDeleteMessage,
|
||||
context: { mode: ['context', 'browser'], group: 'remove' }
|
||||
},
|
||||
chunter.action.DeleteChatMessage
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: chunter.actionImpl.ReplyToThread,
|
||||
label: chunter.string.ReplyToThread,
|
||||
icon: chunter.icon.Thread,
|
||||
input: 'focus',
|
||||
category: chunter.category.Chunter,
|
||||
target: activity.class.ActivityMessage,
|
||||
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
|
||||
},
|
||||
chunter.action.ReplyToThread
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
...viewTemplates.open,
|
||||
target: notification.class.DocNotifyContext,
|
||||
context: {
|
||||
mode: ['browser', 'context'],
|
||||
group: 'create'
|
||||
},
|
||||
action: chunter.actionImpl.OpenChannel
|
||||
},
|
||||
chunter.action.OpenChannel
|
||||
)
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: chunter.class.Channel,
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: chunter.class.DirectMessage,
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: activity.class.DocUpdateMessage,
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: chunter.class.ChatMessage,
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
})
|
||||
}
|
||||
|
||||
export default chunter
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { TxOperations } from '@hcengineering/core'
|
||||
import core, { type Class, type Doc, type Ref, TxOperations } from '@hcengineering/core'
|
||||
import {
|
||||
type MigrateOperation,
|
||||
type MigrationClient,
|
||||
@ -22,14 +22,44 @@ import {
|
||||
} from '@hcengineering/model'
|
||||
import { chunterId } from '@hcengineering/chunter'
|
||||
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
import { DOMAIN_COMMENT } from './index'
|
||||
import chunter from './plugin'
|
||||
|
||||
export async function createGeneral (tx: TxOperations): Promise<void> {
|
||||
export async function createDocNotifyContexts (
|
||||
client: MigrationUpgradeClient,
|
||||
tx: TxOperations,
|
||||
attachedTo: Ref<Doc>,
|
||||
attachedToClass: Ref<Class<Doc>>
|
||||
): Promise<void> {
|
||||
const users = await client.findAll(core.class.Account, {})
|
||||
for (const user of users) {
|
||||
if (user._id === core.account.System) {
|
||||
continue
|
||||
}
|
||||
const docNotifyContext = await client.findOne(notification.class.DocNotifyContext, {
|
||||
user: user._id,
|
||||
attachedTo,
|
||||
attachedToClass
|
||||
})
|
||||
|
||||
if (docNotifyContext === undefined) {
|
||||
await tx.createDoc(notification.class.DocNotifyContext, core.space.Space, {
|
||||
user: user._id,
|
||||
attachedTo,
|
||||
attachedToClass,
|
||||
hidden: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGeneral (client: MigrationUpgradeClient, tx: TxOperations): Promise<void> {
|
||||
const createTx = await tx.findOne(core.class.TxCreateDoc, {
|
||||
objectId: chunter.space.General
|
||||
})
|
||||
|
||||
if (createTx === undefined) {
|
||||
await tx.createDoc(
|
||||
chunter.class.Channel,
|
||||
@ -45,12 +75,15 @@ export async function createGeneral (tx: TxOperations): Promise<void> {
|
||||
chunter.space.General
|
||||
)
|
||||
}
|
||||
|
||||
await createDocNotifyContexts(client, tx, chunter.space.General, chunter.class.Channel)
|
||||
}
|
||||
|
||||
export async function createRandom (tx: TxOperations): Promise<void> {
|
||||
export async function createRandom (client: MigrationUpgradeClient, tx: TxOperations): Promise<void> {
|
||||
const createTx = await tx.findOne(core.class.TxCreateDoc, {
|
||||
objectId: chunter.space.Random
|
||||
})
|
||||
|
||||
if (createTx === undefined) {
|
||||
await tx.createDoc(
|
||||
chunter.class.Channel,
|
||||
@ -66,6 +99,8 @@ export async function createRandom (tx: TxOperations): Promise<void> {
|
||||
chunter.space.Random
|
||||
)
|
||||
}
|
||||
|
||||
await createDocNotifyContexts(client, tx, chunter.space.Random, chunter.class.Channel)
|
||||
}
|
||||
|
||||
async function createBacklink (tx: TxOperations): Promise<void> {
|
||||
@ -104,8 +139,8 @@ export const chunterOperation: MigrateOperation = {
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
await createGeneral(tx)
|
||||
await createRandom(tx)
|
||||
await createGeneral(client, tx)
|
||||
await createRandom(client, tx)
|
||||
await createBacklink(tx)
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,8 @@
|
||||
import type { ActivityMessage, DocUpdateMessageViewlet, TxViewlet } from '@hcengineering/activity'
|
||||
import { chunterId, type Channel } from '@hcengineering/chunter'
|
||||
import chunter from '@hcengineering/chunter-resources/src/plugin'
|
||||
import type { Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { type NotificationGroup } from '@hcengineering/notification'
|
||||
import { type Client, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { type DocNotifyContext, 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'
|
||||
@ -29,11 +29,10 @@ export default mergeIds(chunterId, chunter, {
|
||||
DirectMessagePresenter: '' as AnyComponent,
|
||||
MessagePresenter: '' as AnyComponent,
|
||||
DmPresenter: '' as AnyComponent,
|
||||
Threads: '' as AnyComponent,
|
||||
SavedMessages: '' as AnyComponent,
|
||||
ChunterBrowser: '' as AnyComponent,
|
||||
BacklinkContent: '' as AnyComponent,
|
||||
BacklinkReference: '' as AnyComponent
|
||||
BacklinkReference: '' as AnyComponent,
|
||||
ChannelsPanel: '' as AnyComponent,
|
||||
Chat: '' as AnyComponent
|
||||
},
|
||||
action: {
|
||||
MarkCommentUnread: '' as Ref<Action>,
|
||||
@ -41,17 +40,15 @@ export default mergeIds(chunterId, chunter, {
|
||||
ArchiveChannel: '' as Ref<Action>,
|
||||
UnarchiveChannel: '' as Ref<Action>,
|
||||
ConvertToPrivate: '' as Ref<Action>,
|
||||
CopyCommentLink: '' as Ref<Action<Doc, any>>,
|
||||
CopyThreadMessageLink: '' as Ref<Action<Doc, any>>,
|
||||
CopyMessageLink: '' as Ref<Action<Doc, any>>
|
||||
CopyChatMessageLink: '' as Ref<Action<Doc, any>>,
|
||||
OpenChannel: '' as Ref<Action>
|
||||
},
|
||||
actionImpl: {
|
||||
MarkUnread: '' as ViewAction,
|
||||
MarkCommentUnread: '' as ViewAction,
|
||||
ArchiveChannel: '' as ViewAction,
|
||||
UnarchiveChannel: '' as ViewAction,
|
||||
ConvertDmToPrivateChannel: '' as ViewAction,
|
||||
DeleteChatMessage: '' as ViewAction
|
||||
DeleteChatMessage: '' as ViewAction,
|
||||
ReplyToThread: '' as ViewAction
|
||||
},
|
||||
category: {
|
||||
Chunter: '' as Ref<ActionCategory>
|
||||
@ -62,7 +59,6 @@ export default mergeIds(chunterId, chunter, {
|
||||
Content: '' as IntlString,
|
||||
Comment: '' as IntlString,
|
||||
Reference: '' as IntlString,
|
||||
Chat: '' as IntlString,
|
||||
CreateBy: '' as IntlString,
|
||||
Create: '' as IntlString,
|
||||
Edit: '' as IntlString,
|
||||
@ -71,14 +67,15 @@ export default mergeIds(chunterId, chunter, {
|
||||
MentionNotification: '' as IntlString,
|
||||
PinnedMessages: '' as IntlString,
|
||||
SavedMessages: '' as IntlString,
|
||||
ThreadMessage: '' as IntlString,
|
||||
Emoji: '' as IntlString,
|
||||
FilterBacklinks: '' as IntlString,
|
||||
DM: '' as IntlString,
|
||||
DMNotification: '' as IntlString,
|
||||
ConfigLabel: '' as IntlString,
|
||||
ConfigDescription: '' as IntlString,
|
||||
Reacted: '' as IntlString
|
||||
Reacted: '' as IntlString,
|
||||
Saved: '' as IntlString,
|
||||
RepliedToThread: '' as IntlString
|
||||
},
|
||||
viewlet: {
|
||||
Chat: '' as Ref<ViewletDescriptor>
|
||||
@ -105,9 +102,12 @@ export default mergeIds(chunterId, chunter, {
|
||||
Random: '' as Ref<Channel>
|
||||
},
|
||||
function: {
|
||||
ChunterBrowserVisible: '' as Resource<(spaces: Space[]) => Promise<boolean>>,
|
||||
GetLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
|
||||
GetFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
|
||||
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>>
|
||||
},
|
||||
filter: {
|
||||
BacklinksFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
|
||||
|
@ -243,6 +243,8 @@ export function createModel (builder: Builder): void {
|
||||
]
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.Channel, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: contact.class.Contact,
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
@ -280,6 +282,7 @@ export function createModel (builder: Builder): void {
|
||||
hidden: false,
|
||||
// component: contact.component.ContactsTabs,
|
||||
locationResolver: contact.resolver.Location,
|
||||
aside: chunter.component.ThreadView,
|
||||
navigatorModel: {
|
||||
spaces: [],
|
||||
specials: [
|
||||
@ -988,6 +991,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: contact.class.Person,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
@ -998,6 +1002,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: contact.mixin.Employee,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
@ -1008,6 +1013,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: contact.class.Organization,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
|
@ -148,6 +148,7 @@ export function createModel (builder: Builder): void {
|
||||
icon: inventory.icon.InventoryApplication,
|
||||
alias: inventoryId,
|
||||
hidden: false,
|
||||
aside: chunter.component.ThreadView,
|
||||
navigatorModel: {
|
||||
specials: [
|
||||
{
|
||||
@ -187,6 +188,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: inventory.class.Product,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
@ -197,6 +199,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: inventory.class.Category,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
|
@ -155,6 +155,7 @@ export function createModel (builder: Builder): void {
|
||||
icon: lead.icon.LeadApplication,
|
||||
alias: leadId,
|
||||
hidden: false,
|
||||
aside: chunter.component.ThreadView,
|
||||
navigatorModel: {
|
||||
specials: [
|
||||
{
|
||||
@ -542,6 +543,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: lead.class.Lead,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
|
@ -43,7 +43,8 @@ import {
|
||||
TypeString,
|
||||
UX,
|
||||
TypeBoolean,
|
||||
TypeDate
|
||||
TypeDate,
|
||||
TypeIntlString
|
||||
} from '@hcengineering/model'
|
||||
import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
|
||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||
@ -65,7 +66,9 @@ import {
|
||||
type NotificationTemplate,
|
||||
type NotificationType,
|
||||
notificationId,
|
||||
type NotificationObjectPresenter
|
||||
type NotificationObjectPresenter,
|
||||
type ActivityInboxNotification,
|
||||
type CommonInboxNotification
|
||||
} from '@hcengineering/notification'
|
||||
import { type Asset, type IntlString } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
@ -197,18 +200,13 @@ export class TDocNotifyContext extends TDoc implements DocNotifyContext {
|
||||
@Prop(TypeDate(), core.string.Date)
|
||||
@Index(IndexKind.Indexed)
|
||||
lastUpdateTimestamp?: Timestamp
|
||||
|
||||
@Prop(TypeBoolean(), notification.string.Pinned)
|
||||
isPinned?: boolean
|
||||
}
|
||||
|
||||
@Model(notification.class.InboxNotification, core.class.Doc, DOMAIN_NOTIFICATION)
|
||||
export class TInboxNotification extends TDoc implements InboxNotification {
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedTo)
|
||||
@Index(IndexKind.Indexed)
|
||||
attachedTo!: Ref<ActivityMessage>
|
||||
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedToClass)
|
||||
@Index(IndexKind.Indexed)
|
||||
attachedToClass!: Ref<Class<ActivityMessage>>
|
||||
|
||||
@Prop(TypeRef(notification.class.DocNotifyContext), core.string.AttachedTo)
|
||||
@Index(IndexKind.Indexed)
|
||||
docNotifyContext!: Ref<DocNotifyContext>
|
||||
@ -220,6 +218,32 @@ export class TInboxNotification extends TDoc implements InboxNotification {
|
||||
@Prop(TypeBoolean(), core.string.Boolean)
|
||||
@Index(IndexKind.Indexed)
|
||||
isViewed!: boolean
|
||||
|
||||
title?: IntlString
|
||||
body?: IntlString
|
||||
intlParams?: Record<string, string | number>
|
||||
intlParamsNotLocalized?: Record<string, IntlString>
|
||||
}
|
||||
|
||||
@Model(notification.class.ActivityInboxNotification, notification.class.InboxNotification)
|
||||
export class TActivityInboxNotification extends TInboxNotification implements ActivityInboxNotification {
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedTo)
|
||||
@Index(IndexKind.Indexed)
|
||||
attachedTo!: Ref<ActivityMessage>
|
||||
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedToClass)
|
||||
@Index(IndexKind.Indexed)
|
||||
attachedToClass!: Ref<Class<ActivityMessage>>
|
||||
}
|
||||
|
||||
@Model(notification.class.CommonInboxNotification, notification.class.InboxNotification)
|
||||
export class TCommonInboxNotification extends TInboxNotification implements CommonInboxNotification {
|
||||
@Prop(TypeIntlString(), notification.string.Message)
|
||||
message!: IntlString
|
||||
|
||||
props!: Record<string, any>
|
||||
icon!: Asset
|
||||
iconProps!: Record<string, any>
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
@ -236,7 +260,9 @@ export function createModel (builder: Builder): void {
|
||||
TNotificationObjectPresenter,
|
||||
TNotificationPreview,
|
||||
TDocNotifyContext,
|
||||
TInboxNotification
|
||||
TInboxNotification,
|
||||
TActivityInboxNotification,
|
||||
TCommonInboxNotification
|
||||
)
|
||||
|
||||
// Temporarily disabled, we should think about it
|
||||
@ -283,20 +309,6 @@ export function createModel (builder: Builder): void {
|
||||
notification.ids.NotificationSettings
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
workbench.class.Application,
|
||||
core.space.Model,
|
||||
{
|
||||
label: notification.string.Inbox,
|
||||
icon: notification.icon.Notifications,
|
||||
alias: notificationId,
|
||||
hidden: true,
|
||||
component: notification.component.Inbox,
|
||||
aside: chunter.component.ThreadView
|
||||
},
|
||||
notification.app.Notification
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
workbench.class.Application,
|
||||
core.space.Model,
|
||||
@ -312,7 +324,7 @@ export function createModel (builder: Builder): void {
|
||||
specials: [
|
||||
{
|
||||
id: 'all',
|
||||
component: notification.component.NewInbox,
|
||||
component: notification.component.Inbox,
|
||||
icon: activity.icon.Activity,
|
||||
label: activity.string.AllActivity,
|
||||
componentProps: {
|
||||
@ -322,7 +334,7 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
{
|
||||
id: 'reactions',
|
||||
component: notification.component.NewInbox,
|
||||
component: notification.component.Inbox,
|
||||
icon: activity.icon.Emoji,
|
||||
label: activity.string.Reactions,
|
||||
componentProps: {
|
||||
@ -336,57 +348,6 @@ export function createModel (builder: Builder): void {
|
||||
notification.app.Inbox
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: notification.actionImpl.MarkAsUnread,
|
||||
actionProps: {},
|
||||
label: notification.string.MarkAsUnread,
|
||||
icon: notification.icon.Track,
|
||||
input: 'focus',
|
||||
visibilityTester: notification.function.HasntNotifications,
|
||||
category: notification.category.Notification,
|
||||
target: notification.class.DocUpdates,
|
||||
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
|
||||
},
|
||||
notification.action.MarkAsUnread
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: notification.actionImpl.Hide,
|
||||
actionProps: {},
|
||||
label: notification.string.Archive,
|
||||
icon: view.icon.Archive,
|
||||
input: 'focus',
|
||||
keyBinding: ['Backspace'],
|
||||
category: notification.category.Notification,
|
||||
target: notification.class.DocUpdates,
|
||||
context: { mode: ['context', 'browser'], group: 'edit' }
|
||||
},
|
||||
notification.action.Hide
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: notification.actionImpl.Unsubscribe,
|
||||
actionProps: {},
|
||||
label: notification.string.DontTrack,
|
||||
icon: notification.icon.Hide,
|
||||
input: 'focus',
|
||||
category: notification.category.Notification,
|
||||
target: notification.class.DocUpdates,
|
||||
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
|
||||
},
|
||||
notification.action.Unsubscribe
|
||||
)
|
||||
|
||||
builder.mixin(notification.class.DocUpdates, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [view.action.Delete, view.action.Open]
|
||||
})
|
||||
|
||||
createAction(builder, {
|
||||
action: workbench.actionImpl.Navigate,
|
||||
actionProps: {
|
||||
@ -486,7 +447,7 @@ export function createModel (builder: Builder): void {
|
||||
input: 'focus',
|
||||
visibilityTester: notification.function.HasMarkAsReadAction,
|
||||
category: notification.category.Notification,
|
||||
target: activity.class.ActivityMessage,
|
||||
target: notification.class.InboxNotification,
|
||||
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
|
||||
},
|
||||
notification.action.MarkAsReadInboxNotification
|
||||
@ -501,7 +462,7 @@ export function createModel (builder: Builder): void {
|
||||
input: 'focus',
|
||||
visibilityTester: notification.function.HasMarkAsUnreadAction,
|
||||
category: notification.category.Notification,
|
||||
target: activity.class.ActivityMessage,
|
||||
target: notification.class.InboxNotification,
|
||||
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
|
||||
},
|
||||
notification.action.MarkAsUnreadInboxNotification
|
||||
@ -516,12 +477,89 @@ export function createModel (builder: Builder): void {
|
||||
input: 'focus',
|
||||
keyBinding: ['Backspace'],
|
||||
category: notification.category.Notification,
|
||||
visibilityTester: notification.function.HasDeleteNotificationAction,
|
||||
target: activity.class.ActivityMessage,
|
||||
target: notification.class.InboxNotification,
|
||||
context: { mode: ['context', 'browser'], group: 'edit' }
|
||||
},
|
||||
notification.action.DeleteInboxNotification
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: notification.actionImpl.HideDocNotifyContext,
|
||||
label: view.string.Archive,
|
||||
icon: view.icon.Archive,
|
||||
input: 'focus',
|
||||
category: view.category.General,
|
||||
target: notification.class.DocNotifyContext,
|
||||
context: {
|
||||
mode: ['browser', 'context'],
|
||||
group: 'remove'
|
||||
},
|
||||
visibilityTester: notification.function.IsDocNotifyContextVisible
|
||||
},
|
||||
notification.action.HideDocNotifyContext
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: notification.actionImpl.UnHideDocNotifyContext,
|
||||
label: view.string.UnArchive,
|
||||
icon: view.icon.Archive,
|
||||
input: 'focus',
|
||||
category: view.category.General,
|
||||
target: notification.class.DocNotifyContext,
|
||||
context: {
|
||||
mode: ['browser', 'context'],
|
||||
group: 'remove'
|
||||
},
|
||||
visibilityTester: notification.function.IsDocNotifyContextHidden
|
||||
},
|
||||
notification.action.UnHideDocNotifyContext
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: notification.actionImpl.PinDocNotifyContext,
|
||||
label: view.string.Pin,
|
||||
icon: notification.icon.Track,
|
||||
input: 'focus',
|
||||
category: notification.category.Notification,
|
||||
target: notification.class.DocNotifyContext,
|
||||
visibilityTester: notification.function.HasDocNotifyContextPinAction,
|
||||
context: { mode: ['context', 'browser'], group: 'edit' }
|
||||
},
|
||||
notification.action.PinDocNotifyContext
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: notification.actionImpl.UnpinDocNotifyContext,
|
||||
label: view.string.Unpin,
|
||||
icon: notification.icon.Track,
|
||||
input: 'focus',
|
||||
category: notification.category.Notification,
|
||||
target: notification.class.DocNotifyContext,
|
||||
visibilityTester: notification.function.HasDocNotifyContextUnpinAction,
|
||||
context: { mode: ['context', 'browser'], group: 'edit' }
|
||||
},
|
||||
notification.action.UnpinDocNotifyContext
|
||||
)
|
||||
|
||||
builder.mixin(notification.class.DocNotifyContext, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: notification.component.DocNotifyContextPresenter
|
||||
})
|
||||
|
||||
builder.mixin(notification.class.ActivityInboxNotification, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: notification.component.ActivityInboxNotificationPresenter
|
||||
})
|
||||
|
||||
builder.mixin(notification.class.CommonInboxNotification, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: notification.component.CommonInboxNotificationPresenter
|
||||
})
|
||||
}
|
||||
|
||||
export function generateClassNotificationTypes (
|
||||
|
@ -13,9 +13,41 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { TxOperations } from '@hcengineering/core'
|
||||
import { type MigrateOperation, type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model'
|
||||
import notification from '@hcengineering/notification'
|
||||
import core, {
|
||||
type AttachedDoc,
|
||||
type Doc,
|
||||
type Domain,
|
||||
DOMAIN_TX,
|
||||
generateId,
|
||||
type Ref,
|
||||
type TxCollectionCUD,
|
||||
type TxCUD,
|
||||
TxOperations,
|
||||
TxProcessor
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
type MigrateOperation,
|
||||
type MigrationClient,
|
||||
type MigrationUpgradeClient,
|
||||
tryMigrate
|
||||
} from '@hcengineering/model'
|
||||
import notification, {
|
||||
type DocNotifyContext,
|
||||
type DocUpdates,
|
||||
type DocUpdateTx,
|
||||
type InboxNotification,
|
||||
notificationId
|
||||
} from '@hcengineering/notification'
|
||||
import activity, { type ActivityMessage, type DocUpdateMessage } from '@hcengineering/activity'
|
||||
|
||||
import { DOMAIN_NOTIFICATION } from './index'
|
||||
|
||||
interface InboxData {
|
||||
context: DocNotifyContext
|
||||
notifications: InboxNotification[]
|
||||
}
|
||||
|
||||
const DOMAIN_ACTIVITY = 'activity' as Domain
|
||||
|
||||
async function createSpace (client: MigrationUpgradeClient): Promise<void> {
|
||||
const txop = new TxOperations(client, core.account.System)
|
||||
@ -38,8 +70,140 @@ async function createSpace (client: MigrationUpgradeClient): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function getActivityMessages (
|
||||
client: MigrationClient,
|
||||
tx: DocUpdateTx,
|
||||
context: DocNotifyContext
|
||||
): Promise<ActivityMessage[]> {
|
||||
const docUpdateMessages = await client.find<DocUpdateMessage>(DOMAIN_ACTIVITY, {
|
||||
_class: activity.class.DocUpdateMessage,
|
||||
txId: tx._id,
|
||||
attachedTo: context.attachedTo
|
||||
})
|
||||
|
||||
if (docUpdateMessages.length > 0) {
|
||||
return docUpdateMessages
|
||||
}
|
||||
|
||||
const originTx = (await client.find<TxCUD<Doc>>(DOMAIN_TX, { _id: tx._id }))[0]
|
||||
|
||||
if (originTx === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const innerTx = TxProcessor.extractTx(originTx as TxCollectionCUD<Doc, AttachedDoc>) as TxCUD<Doc>
|
||||
|
||||
return (
|
||||
await client.find<ActivityMessage>(DOMAIN_ACTIVITY, {
|
||||
_id: innerTx.objectId as Ref<ActivityMessage>,
|
||||
attachedTo: context.attachedTo
|
||||
})
|
||||
).filter(({ _class }) => client.hierarchy.isDerived(_class, activity.class.ActivityMessage))
|
||||
}
|
||||
|
||||
async function getInboxNotifications (
|
||||
client: MigrationClient,
|
||||
tx: DocUpdateTx,
|
||||
context: DocNotifyContext
|
||||
): Promise<InboxNotification[]> {
|
||||
const messages = await getActivityMessages(client, tx, context)
|
||||
|
||||
if (messages.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return messages.map((message) => ({
|
||||
_id: generateId(),
|
||||
_class: notification.class.InboxNotification,
|
||||
space: context.space,
|
||||
user: context.user,
|
||||
isViewed: !tx.isNew,
|
||||
attachedTo: message._id,
|
||||
attachedToClass: message._class,
|
||||
docNotifyContext: context._id,
|
||||
title: tx.title,
|
||||
body: tx.body,
|
||||
intlParams: tx.intlParams,
|
||||
intlParamsNotLocalized: tx.intlParamsNotLocalized,
|
||||
modifiedOn: tx.modifiedOn,
|
||||
modifiedBy: tx.modifiedBy,
|
||||
createdOn: tx.modifiedOn,
|
||||
createdBy: tx.modifiedBy
|
||||
}))
|
||||
}
|
||||
|
||||
async function getInboxData (client: MigrationClient, docUpdate: DocUpdates): Promise<InboxData | undefined> {
|
||||
if (!client.hierarchy.hasClass(docUpdate.attachedToClass)) {
|
||||
console.log('cannot find class: ', docUpdate.attachedToClass)
|
||||
return
|
||||
}
|
||||
|
||||
const { txes } = docUpdate
|
||||
const newTxIndex = txes.findIndex(({ isNew }) => isNew)
|
||||
|
||||
const context: DocNotifyContext = {
|
||||
_id: docUpdate._id,
|
||||
_class: notification.class.DocNotifyContext,
|
||||
space: docUpdate.space,
|
||||
user: docUpdate.user,
|
||||
attachedTo: docUpdate.attachedTo,
|
||||
attachedToClass: docUpdate.attachedToClass,
|
||||
hidden: docUpdate.hidden,
|
||||
lastViewedTimestamp: newTxIndex !== -1 ? txes[newTxIndex - 1]?.modifiedOn : docUpdate.lastTxTime,
|
||||
lastUpdateTimestamp: docUpdate.lastTxTime,
|
||||
modifiedBy: docUpdate.modifiedBy,
|
||||
modifiedOn: docUpdate.modifiedOn,
|
||||
createdBy: docUpdate.createdBy,
|
||||
createdOn: docUpdate.createdOn
|
||||
}
|
||||
|
||||
const notifications = (await Promise.all(txes.map((tx) => getInboxNotifications(client, tx, context)))).flat()
|
||||
|
||||
return {
|
||||
context,
|
||||
notifications
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateInboxNotifications (client: MigrationClient): Promise<void> {
|
||||
while (true) {
|
||||
const docUpdates = await client.find<DocUpdates>(
|
||||
DOMAIN_NOTIFICATION,
|
||||
{
|
||||
_class: notification.class.DocUpdates
|
||||
},
|
||||
{ limit: 500 }
|
||||
)
|
||||
|
||||
if (docUpdates.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const data: InboxData[] = (
|
||||
await Promise.all(docUpdates.map((docUpdate) => getInboxData(client, docUpdate)))
|
||||
).filter((data): data is InboxData => data !== undefined)
|
||||
|
||||
await client.deleteMany(DOMAIN_NOTIFICATION, { _id: { $in: docUpdates.map(({ _id }) => _id) } })
|
||||
await client.create(
|
||||
DOMAIN_NOTIFICATION,
|
||||
data.map(({ context }) => context)
|
||||
)
|
||||
await client.create(
|
||||
DOMAIN_NOTIFICATION,
|
||||
data.flatMap(({ notifications }) => notifications)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const notificationOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {},
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, notificationId, [
|
||||
{
|
||||
state: 'inbox-notifications',
|
||||
func: migrateInboxNotifications
|
||||
}
|
||||
])
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
await createSpace(client)
|
||||
}
|
||||
|
@ -31,7 +31,8 @@ export default mergeIds(notificationId, notification, {
|
||||
Archive: '' as IntlString,
|
||||
MarkAsUnread: '' as IntlString,
|
||||
MarkAsRead: '' as IntlString,
|
||||
ChangeCollaborators: '' as IntlString
|
||||
ChangeCollaborators: '' as IntlString,
|
||||
Message: '' as IntlString
|
||||
},
|
||||
app: {
|
||||
Notification: '' as Ref<Application>,
|
||||
@ -48,31 +49,30 @@ export default mergeIds(notificationId, notification, {
|
||||
component: {
|
||||
NotificationSettings: '' as AnyComponent,
|
||||
InboxAside: '' as AnyComponent,
|
||||
ChatMessagePresenter: '' as AnyComponent,
|
||||
DocUpdateMessagePresenter: '' as AnyComponent,
|
||||
PinMessageAction: '' as AnyComponent
|
||||
ActivityInboxNotificationPresenter: '' as AnyComponent,
|
||||
CommonInboxNotificationPresenter: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
HasntNotifications: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
HasMarkAsUnreadAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
HasMarkAsReadAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
HasDeleteNotificationAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
|
||||
HasDocNotifyContextPinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
HasDocNotifyContextUnpinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
|
||||
},
|
||||
category: {
|
||||
Notification: '' as Ref<ActionCategory>
|
||||
},
|
||||
groups: {},
|
||||
action: {
|
||||
Unsubscribe: '' as Ref<Action>,
|
||||
Hide: '' as Ref<Action>,
|
||||
MarkAsUnread: '' as Ref<Action>
|
||||
Unsubscribe: '' as Ref<Action>
|
||||
},
|
||||
actionImpl: {
|
||||
Unsubscribe: '' as ViewAction,
|
||||
Hide: '' as ViewAction,
|
||||
MarkAsUnread: '' as ViewAction,
|
||||
MarkAsUnreadInboxNotification: '' as ViewAction,
|
||||
MarkAsReadInboxNotification: '' as ViewAction,
|
||||
DeleteInboxNotification: '' as ViewAction
|
||||
DeleteInboxNotification: '' as ViewAction,
|
||||
UnpinDocNotifyContext: '' as ViewAction,
|
||||
PinDocNotifyContext: '' as ViewAction,
|
||||
HideDocNotifyContext: '' as ViewAction,
|
||||
UnHideDocNotifyContext: '' as ViewAction
|
||||
}
|
||||
})
|
||||
|
@ -298,6 +298,7 @@ export function createModel (builder: Builder): void {
|
||||
locationResolver: recruit.resolver.Location,
|
||||
alias: recruitId,
|
||||
hidden: false,
|
||||
aside: chunter.component.ThreadView,
|
||||
navigatorModel: {
|
||||
spaces: [],
|
||||
specials: [
|
||||
@ -1619,6 +1620,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: recruit.class.Vacancy,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
@ -1629,6 +1631,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: recruit.class.Applicant,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
@ -1639,6 +1642,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: recruit.class.Review,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
|
@ -28,6 +28,7 @@
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/model": "^0.6.7",
|
||||
"@hcengineering/model-activity": "^0.6.0",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/server-activity": "^0.6.0",
|
||||
"@hcengineering/server-activity-resources": "^0.6.0",
|
||||
|
@ -22,15 +22,13 @@ export { activityServerOperation } from './migration'
|
||||
export { serverActivityId } from '@hcengineering/server-activity'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
// NOTE: temporarily disabled
|
||||
// builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
// trigger: serverNotification.trigger.OnReactionChanged,
|
||||
// txMatch: {
|
||||
// collection: 'reactions',
|
||||
// objectClass: activity.class.ActivityMessage,
|
||||
// _class: core.class.TxCollectionCUD
|
||||
// }
|
||||
// })
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverActivity.trigger.OnReactionChanged,
|
||||
txMatch: {
|
||||
collection: 'reactions',
|
||||
_class: core.class.TxCollectionCUD
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverActivity.trigger.ActivityMessagesHandler
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
serverActivityId
|
||||
} from '@hcengineering/server-activity'
|
||||
import { generateDocUpdateMessages } from '@hcengineering/server-activity-resources'
|
||||
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
|
||||
|
||||
function getActivityControl (client: MigrationClient): ActivityControl {
|
||||
const txFactory = new TxFactory(core.account.System, false)
|
||||
@ -55,6 +56,16 @@ async function generateDocUpdateMessageByTx (
|
||||
client: MigrationClient,
|
||||
objectCache?: DocObjectCache
|
||||
): Promise<void> {
|
||||
const existsMessages = await client.find<DocUpdateMessage>(
|
||||
DOMAIN_ACTIVITY,
|
||||
{ _class: activity.class.DocUpdateMessage, txId: tx._id },
|
||||
{ projection: { _id: 1 } }
|
||||
)
|
||||
|
||||
if (existsMessages.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const createCollectionCUDTxes = await generateDocUpdateMessages(tx, control, undefined, undefined, objectCache)
|
||||
|
||||
for (const collectionTx of createCollectionCUDTxes) {
|
||||
@ -206,8 +217,14 @@ export const activityServerOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, serverActivityId, [
|
||||
{
|
||||
state: 'activity-messages',
|
||||
func: createDocUpdateMessages
|
||||
state: 'doc-update-messages',
|
||||
func: async (client) => {
|
||||
// Recreate activity to avoid duplicates
|
||||
await client.deleteMany(DOMAIN_ACTIVITY, {
|
||||
_class: activity.class.DocUpdateMessage
|
||||
})
|
||||
await createDocUpdateMessages(client)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -21,6 +21,7 @@ import serverNotification from '@hcengineering/server-notification'
|
||||
import serverCore, { type ObjectDDParticipant } from '@hcengineering/server-core'
|
||||
import serverChunter from '@hcengineering/server-chunter'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
export { serverChunterId } from '@hcengineering/server-chunter'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
@ -45,7 +46,7 @@ export function createModel (builder: Builder): void {
|
||||
presenter: serverChunter.function.ChunterNotificationContentProvider
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Message, core.class.Class, serverNotification.mixin.NotificationPresenter, {
|
||||
builder.mixin(chunter.class.ChatMessage, core.class.Class, serverNotification.mixin.NotificationPresenter, {
|
||||
presenter: serverChunter.function.ChunterNotificationContentProvider
|
||||
})
|
||||
|
||||
@ -58,11 +59,22 @@ export function createModel (builder: Builder): void {
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverChunter.trigger.OnMessageSent,
|
||||
trigger: serverChunter.trigger.OnDirectMessageSent,
|
||||
txMatch: {
|
||||
objectClass: chunter.class.DirectMessage,
|
||||
_class: core.class.TxCollectionCUD,
|
||||
collection: 'messages'
|
||||
objectClass: chunter.class.DirectMessage,
|
||||
collection: 'messages',
|
||||
'tx._class': core.class.TxCreateDoc,
|
||||
'tx.objectClass': chunter.class.ChatMessage
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverChunter.trigger.OnChatMessageRemoved,
|
||||
txMatch: {
|
||||
_class: core.class.TxCollectionCUD,
|
||||
'tx._class': core.class.TxRemoveDoc,
|
||||
'tx.objectClass': chunter.class.ChatMessage
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -31,6 +31,7 @@ import serverNotification, {
|
||||
type NotificationContentProvider
|
||||
} from '@hcengineering/server-notification'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import activity from '@hcengineering/activity'
|
||||
|
||||
export { serverNotificationId } from '@hcengineering/server-notification'
|
||||
|
||||
@ -60,17 +61,36 @@ export function createModel (builder: Builder): void {
|
||||
builder.createModel(THTMLPresenter, TTextPresenter, TTypeMatch, TNotificationPresenter)
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnBacklinkCreate
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.NotificationMessagesHandler
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnChatMessageSent,
|
||||
trigger: serverNotification.trigger.ActivityNotificationsHandler,
|
||||
txMatch: {
|
||||
objectClass: chunter.class.ChatMessage
|
||||
_class: core.class.TxCollectionCUD,
|
||||
'tx._class': core.class.TxCreateDoc,
|
||||
'tx.objectClass': activity.class.DocUpdateMessage
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnActivityNotificationViewed,
|
||||
txMatch: {
|
||||
_class: core.class.TxUpdateDoc,
|
||||
objectClass: notification.class.ActivityInboxNotification
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnChatMessageCreate,
|
||||
txMatch: {
|
||||
_class: core.class.TxCollectionCUD,
|
||||
'tx._class': core.class.TxCreateDoc,
|
||||
'tx.objectClass': chunter.class.ChatMessage
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnBacklinkCreate,
|
||||
txMatch: {
|
||||
_class: core.class.TxCollectionCUD,
|
||||
'tx._class': core.class.TxCreateDoc
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -161,6 +161,24 @@ function defineFilters (builder: Builder): void {
|
||||
getVisibleFilters: tracker.function.GetVisibleFilters
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectIdentifier, {
|
||||
provider: tracker.function.IssueTitleProvider
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Issue, core.class.Class, chunter.mixin.ObjectChatPanel, {
|
||||
ignoreKeys: [
|
||||
'number',
|
||||
'createdBy',
|
||||
'attachedTo',
|
||||
'title',
|
||||
'collaborators',
|
||||
'description',
|
||||
'remainingTime',
|
||||
'reportedTime'
|
||||
],
|
||||
titleProvider: tracker.function.IssueChatTitleProvider
|
||||
})
|
||||
|
||||
//
|
||||
// Issue Status
|
||||
//
|
||||
@ -256,6 +274,7 @@ function defineApplication (
|
||||
alias: trackerId,
|
||||
hidden: false,
|
||||
locationResolver: tracker.resolver.Location,
|
||||
aside: chunter.component.ThreadView,
|
||||
navigatorModel: {
|
||||
specials: [
|
||||
{
|
||||
@ -607,6 +626,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: tracker.class.Issue,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
@ -617,6 +637,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: tracker.class.IssueTemplate,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
@ -627,6 +648,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: tracker.class.Component,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
@ -637,6 +659,7 @@ export function createModel (builder: Builder): void {
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
messageClass: chunter.class.ChatMessage,
|
||||
objectClass: tracker.class.Milestone,
|
||||
label: chunter.string.LeftComment
|
||||
},
|
||||
|
@ -99,7 +99,9 @@ export default mergeIds(viewId, view, {
|
||||
General: '' as IntlString,
|
||||
Navigation: '' as IntlString,
|
||||
Editor: '' as IntlString,
|
||||
MarkdownFormatting: '' as IntlString
|
||||
MarkdownFormatting: '' as IntlString,
|
||||
Pin: '' as IntlString,
|
||||
Unpin: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
FilterArrayAllResult: '' as FilterFunction,
|
||||
|
@ -22,8 +22,6 @@ import workbench from '@hcengineering/workbench-resources/src/plugin'
|
||||
export default mergeIds(workbenchId, workbench, {
|
||||
component: {
|
||||
ApplicationPresenter: '' as AnyComponent,
|
||||
Archive: '' as AnyComponent,
|
||||
SpaceBrowser: '' as AnyComponent,
|
||||
SpecialView: '' as AnyComponent,
|
||||
ServerManager: '' as AnyComponent
|
||||
},
|
||||
|
@ -1,11 +1,23 @@
|
||||
function isToday (time: number): boolean {
|
||||
const current = new Date()
|
||||
const target = new Date(time)
|
||||
return (
|
||||
current.getDate() === target.getDate() &&
|
||||
current.getMonth() === target.getMonth() &&
|
||||
current.getFullYear() === target.getFullYear()
|
||||
)
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
import { Timestamp } from './classes'
|
||||
|
||||
export function getDay (time: Timestamp): Timestamp {
|
||||
const date: Date = new Date(time)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
return date.getTime()
|
||||
}
|
||||
|
||||
export function getDisplayTime (time: number): string {
|
||||
@ -20,3 +32,17 @@ export function getDisplayTime (time: number): string {
|
||||
|
||||
return new Date(time).toLocaleString('default', options)
|
||||
}
|
||||
|
||||
export function isOtherDay (time1: Timestamp, time2: Timestamp): boolean {
|
||||
return getDay(time1) !== getDay(time2)
|
||||
}
|
||||
|
||||
function isToday (time: number): boolean {
|
||||
const current = new Date()
|
||||
const target = new Date(time)
|
||||
return (
|
||||
current.getDate() === target.getDate() &&
|
||||
current.getMonth() === target.getMonth() &&
|
||||
current.getFullYear() === target.getFullYear()
|
||||
)
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ export interface MigrationClient {
|
||||
|
||||
create: <T extends Doc>(domain: Domain, doc: T | T[]) => Promise<void>
|
||||
delete: <T extends Doc>(domain: Domain, _id: Ref<T>) => Promise<void>
|
||||
deleteMany: <T extends Doc>(domain: Domain, ids: Ref<T>[]) => Promise<void>
|
||||
deleteMany: <T extends Doc>(domain: Domain, query: DocumentQuery<T>) => Promise<void>
|
||||
|
||||
hierarchy: Hierarchy
|
||||
model: ModelDb
|
||||
|
@ -48,6 +48,7 @@
|
||||
dispatch('close')
|
||||
}}
|
||||
/>
|
||||
<div class="mr-4" />
|
||||
<Button
|
||||
label={presentation.string.Cancel}
|
||||
on:click={() => {
|
||||
|
@ -64,6 +64,7 @@
|
||||
on:action
|
||||
on:valid
|
||||
on:validate
|
||||
on:submit
|
||||
>
|
||||
<slot />
|
||||
</Ctor>
|
||||
@ -81,6 +82,7 @@
|
||||
on:action
|
||||
on:valid
|
||||
on:validate
|
||||
on:submit
|
||||
/>
|
||||
{/if}
|
||||
</ErrorBoundary>
|
||||
|
@ -9,4 +9,7 @@
|
||||
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"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol id="bookmark" viewBox="0 0 24 24">
|
||||
<path d="M20,4.2h-2.2V2c0-0.4-0.3-0.8-0.8-0.8H4C3.6,1.2,3.2,1.6,3.2,2v17c0,0.3,0.1,0.5,0.4,0.6c0.2,0.1,0.5,0.1,0.7,0 l1.9-1V22c0,0.3,0.1,0.5,0.4,0.6c0.2,0.1,0.5,0.2,0.7,0l6.2-3l6.2,3c0.2,0.1,0.5,0.1,0.7,0c0.2-0.1,0.4-0.4,0.4-0.6V5 C20.8,4.6,20.4,4.2,20,4.2z M4.8,17.8v-15h11.5v1.5H7C6.6,4.2,6.2,4.6,6.2,5v12L4.8,17.8z M19.2,20.8l-5.4-2.6 c-0.2-0.1-0.4-0.1-0.7,0l-5.4,2.6V5.8h11.5V20.8z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.2 KiB |
@ -17,12 +17,15 @@
|
||||
"For": "For",
|
||||
"From": "from",
|
||||
"In": "In",
|
||||
"LastReply": "Last reply",
|
||||
"New": "New",
|
||||
"NewestFirst": "Newest first",
|
||||
"Pinned": "Pinned",
|
||||
"Reacted": "Reacted",
|
||||
"Reactions": "Reactions",
|
||||
"Removed": "removed",
|
||||
"Replies": "Replies",
|
||||
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
|
||||
"Set": "set",
|
||||
"To": "to",
|
||||
"Unset": "Unset",
|
||||
|
@ -17,12 +17,15 @@
|
||||
"For": "Для",
|
||||
"From": "из",
|
||||
"In": "В",
|
||||
"LastReply": "Последний ответ",
|
||||
"New": "Новые",
|
||||
"NewestFirst": "Сначала новые",
|
||||
"Pinned": "Закрепленные",
|
||||
"Reacted": "Отреагировал(а)",
|
||||
"Reactions": "Реакции",
|
||||
"Removed": "Удалил(а)",
|
||||
"Replies": "Ответы",
|
||||
"RepliesCount": "{replies, plural, one {# ответ} few {# ответа} other {# ответов}}",
|
||||
"Set": "установлен",
|
||||
"To": "на",
|
||||
"Unset": "Cбросил",
|
||||
|
@ -19,5 +19,6 @@ import { loadMetadata } from '@hcengineering/platform'
|
||||
const icons = require('../assets/icons.svg') as string // eslint-disable-line
|
||||
loadMetadata(activity.icon, {
|
||||
Activity: `${icons}#activity`,
|
||||
Emoji: `${icons}#emoji`
|
||||
Emoji: `${icons}#emoji`,
|
||||
Bookmark: `${icons}#bookmark`
|
||||
})
|
||||
|
@ -39,7 +39,9 @@
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/contact-resources": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/preference": "^0.6.9",
|
||||
"@hcengineering/presentation": "^0.6.2",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/view": "^0.6.9",
|
||||
|
@ -22,18 +22,20 @@ import core, {
|
||||
groupByArray,
|
||||
type Hierarchy,
|
||||
type Ref,
|
||||
SortingOrder
|
||||
SortingOrder,
|
||||
type Timestamp
|
||||
} from '@hcengineering/core'
|
||||
import view, { type AttributeModel } from '@hcengineering/view'
|
||||
import { getClient, getFiltredKeys } from '@hcengineering/presentation'
|
||||
import { buildRemovedDoc, getAttributePresenter, getDocLinkTitle } from '@hcengineering/view-resources'
|
||||
import { type Person } from '@hcengineering/contact'
|
||||
import { type IntlString } from '@hcengineering/platform'
|
||||
import { getResource, type IntlString } from '@hcengineering/platform'
|
||||
import { type AnyComponent } from '@hcengineering/ui'
|
||||
import { get } from 'svelte/store'
|
||||
import { personAccountByIdStore } from '@hcengineering/contact-resources'
|
||||
import activity, {
|
||||
type ActivityMessage,
|
||||
type ActivityMessagesFilter,
|
||||
type DisplayActivityMessage,
|
||||
type DisplayDocUpdateMessage,
|
||||
type DocAttributeUpdates,
|
||||
@ -381,6 +383,10 @@ export function pinnedFilter (message: ActivityMessage, _class?: Ref<Doc>): bool
|
||||
return message.isPinned === true
|
||||
}
|
||||
|
||||
export function allFilter (): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
export interface LinkData {
|
||||
title?: string
|
||||
preposition: IntlString
|
||||
@ -427,3 +433,57 @@ export async function getLinkData (
|
||||
object: linkObject
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMessageFragment (doc: Doc): Promise<string> {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
let clazz = hierarchy.getClass(doc._class)
|
||||
let label = clazz.shortLabel
|
||||
while (label === undefined && clazz.extends !== undefined) {
|
||||
clazz = hierarchy.getClass(clazz.extends)
|
||||
label = clazz.shortLabel
|
||||
}
|
||||
label = label ?? doc._class
|
||||
return `${label}-${doc._id}`
|
||||
}
|
||||
|
||||
export async function filterActivityMessages (
|
||||
messages: DisplayActivityMessage[],
|
||||
filters: ActivityMessagesFilter[],
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
filterId?: Ref<ActivityMessagesFilter>
|
||||
): Promise<DisplayActivityMessage[]> {
|
||||
if (filterId === undefined || filterId === activity.ids.AllFilter) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const filter = filters.find(({ _id }) => _id === filterId)
|
||||
|
||||
if (filter === undefined) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const filterFn = await getResource(filter.filter)
|
||||
|
||||
return messages.filter((message) => filterFn(message, objectClass))
|
||||
}
|
||||
|
||||
export function getClosestDateSelectorDate (date: Timestamp, scrollElement: HTMLDivElement): Timestamp | undefined {
|
||||
const dateSelectors = scrollElement.getElementsByClassName('dateSelector')
|
||||
|
||||
if (dateSelectors === undefined || dateSelectors.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let closestDate: Timestamp | undefined = parseInt(dateSelectors[dateSelectors.length - 1].id)
|
||||
|
||||
for (const elem of Array.from(dateSelectors).reverse()) {
|
||||
const curDate = parseInt(elem.id)
|
||||
if (curDate < date) break
|
||||
else if (curDate - date < closestDate - date) {
|
||||
closestDate = curDate
|
||||
}
|
||||
}
|
||||
|
||||
return closestDate
|
||||
}
|
||||
|
@ -24,5 +24,5 @@
|
||||
</script>
|
||||
|
||||
{#if extension}
|
||||
<Component is={extension.components[kind]} {props} on:close on:open />
|
||||
<Component is={extension.components[kind]} {props} on:close on:open on:submit />
|
||||
{/if}
|
||||
|
@ -30,12 +30,17 @@
|
||||
export let object: Doc
|
||||
export let isNewestFirst = false
|
||||
|
||||
let filtered: ActivityMessage[]
|
||||
const allId = activity.ids.AllFilter
|
||||
const client = getClient()
|
||||
|
||||
let filtered: ActivityMessage[]
|
||||
let filters: ActivityMessagesFilter[] = []
|
||||
|
||||
const saved = localStorage.getItem('activity-filter')
|
||||
let selectedFiltersRefs: Ref<Doc>[] | 'All' =
|
||||
saved !== null && saved !== undefined ? (JSON.parse(saved) as Ref<Doc>[] | 'All') : 'All'
|
||||
|
||||
let selectedFiltersRefs: Ref<ActivityMessagesFilter>[] | Ref<ActivityMessagesFilter> = saved
|
||||
? (JSON.parse(saved) as Ref<ActivityMessagesFilter>[])
|
||||
: allId
|
||||
let selectedFilters: ActivityMessagesFilter[] = []
|
||||
|
||||
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs))
|
||||
@ -45,14 +50,14 @@
|
||||
filters = res
|
||||
|
||||
if (saved !== null && saved !== undefined) {
|
||||
const temp: Ref<Doc>[] | 'All' = JSON.parse(saved)
|
||||
if (temp !== 'All' && Array.isArray(temp)) {
|
||||
const temp: Ref<ActivityMessagesFilter>[] | Ref<ActivityMessagesFilter> = JSON.parse(saved)
|
||||
if (temp !== allId && Array.isArray(temp)) {
|
||||
selectedFiltersRefs = temp.filter((it) => filters.findIndex((f) => it === f._id) > -1)
|
||||
if (selectedFiltersRefs.length === 0) {
|
||||
selectedFiltersRefs = 'All'
|
||||
selectedFiltersRefs = allId
|
||||
}
|
||||
} else {
|
||||
selectedFiltersRefs = 'All'
|
||||
selectedFiltersRefs = allId
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -69,9 +74,9 @@
|
||||
isNewestFirst = res.value
|
||||
return
|
||||
}
|
||||
const selected = res.value as Ref<Doc>[]
|
||||
const selected = res.value as Ref<ActivityMessagesFilter>[]
|
||||
const isAll = selected.length === filters.length || selected.length === 0
|
||||
if (res.action === 'select') selectedFiltersRefs = isAll ? 'All' : selected
|
||||
if (res.action === 'select') selectedFiltersRefs = isAll ? allId : selected
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -81,14 +86,14 @@
|
||||
async function updateFilterActions (
|
||||
messages: ActivityMessage[],
|
||||
filters: ActivityMessagesFilter[],
|
||||
selected: Ref<Doc>[] | 'All',
|
||||
selected: Ref<Doc>[] | Ref<ActivityMessagesFilter>,
|
||||
sortOrder: SortingOrder
|
||||
): Promise<void> {
|
||||
const sortedMessages = sortActivityMessages(messages, sortOrder).sort(({ isPinned }) =>
|
||||
isPinned && sortOrder === SortingOrder.Ascending ? -1 : 1
|
||||
)
|
||||
|
||||
if (selected === 'All') {
|
||||
if (selected === allId) {
|
||||
filtered = sortedMessages
|
||||
|
||||
dispatch('update', filtered)
|
||||
@ -126,9 +131,9 @@
|
||||
<div
|
||||
class="tag-icon"
|
||||
on:click={() => {
|
||||
if (selectedFiltersRefs !== 'All') {
|
||||
if (selectedFiltersRefs !== allId && Array.isArray(selectedFiltersRefs)) {
|
||||
const ids = selectedFiltersRefs.filter((it) => it !== filter._id)
|
||||
selectedFiltersRefs = ids.length > 0 ? ids : 'All'
|
||||
selectedFiltersRefs = ids.length > 0 ? ids : allId
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -0,0 +1,356 @@
|
||||
<!--
|
||||
// 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 { Class, Doc, Ref, SortingOrder, isOtherDay, Timestamp } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, {
|
||||
ActivityExtension,
|
||||
ActivityMessage,
|
||||
ActivityMessagesFilter,
|
||||
DisplayActivityMessage,
|
||||
type DisplayDocUpdateMessage
|
||||
} from '@hcengineering/activity'
|
||||
import notification, { InboxNotificationsClient } from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
import ActivityMessagePresenter from './activity-message/ActivityMessagePresenter.svelte'
|
||||
import { combineActivityMessages, filterActivityMessages, getClosestDateSelectorDate } from '../activityMessagesUtils'
|
||||
import ActivityMessagesSeparator from './activity-message/ActivityMessagesSeparator.svelte'
|
||||
import JumpToDateSelector from './JumpToDateSelector.svelte'
|
||||
import ActivityExtensionComponent from './ActivityExtension.svelte'
|
||||
|
||||
export let _class: Ref<Class<ActivityMessage>> = activity.class.ActivityMessage
|
||||
export let object: Doc
|
||||
export let isLoading = false
|
||||
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
||||
export let scrollElement: HTMLDivElement | undefined = undefined
|
||||
export let startFromBottom = false
|
||||
export let filter: Ref<ActivityMessagesFilter> | undefined = undefined
|
||||
export let withDates: boolean = true
|
||||
export let collection: string | undefined = undefined
|
||||
export let showEmbedded = false
|
||||
export let skipLabels = false
|
||||
export let lastViewedTimestamp: Timestamp | undefined = undefined
|
||||
|
||||
const dateSelectorHeight = 30
|
||||
const headerHeight = 50
|
||||
|
||||
const client = getClient()
|
||||
const messagesQuery = createQuery()
|
||||
|
||||
let prevMessagesLength = 0
|
||||
let messages: DisplayActivityMessage[] = []
|
||||
let displayMessages: DisplayActivityMessage[] = []
|
||||
let filters: ActivityMessagesFilter[] = []
|
||||
let extensions: ActivityExtension[] = []
|
||||
|
||||
let separatorElement: HTMLDivElement | undefined = undefined
|
||||
let separatorPosition: number | undefined = undefined
|
||||
let showDateSelector = false
|
||||
let selectedDate: Timestamp | undefined = undefined
|
||||
|
||||
let isViewportInitialized = false
|
||||
|
||||
let inboxClient: InboxNotificationsClient | undefined = undefined
|
||||
|
||||
getResource(notification.function.GetInboxNotificationsClient).then((getClientFn) => {
|
||||
inboxClient = getClientFn()
|
||||
})
|
||||
|
||||
$: client.findAll(activity.class.ActivityExtension, { ofClass: object._class }).then((res) => {
|
||||
extensions = res
|
||||
})
|
||||
|
||||
client.findAll(activity.class.ActivityMessagesFilter, {}).then((res) => {
|
||||
filters = res
|
||||
})
|
||||
|
||||
$: messagesQuery.query(
|
||||
_class,
|
||||
{ attachedTo: object._id },
|
||||
(result: ActivityMessage[]) => {
|
||||
prevMessagesLength = messages.length
|
||||
messages = combineActivityMessages(result)
|
||||
isLoading = false
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
createdOn: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function scrollToBottom (afterScrollFn?: () => void) {
|
||||
setTimeout(() => {
|
||||
if (scrollElement !== undefined) {
|
||||
scrollElement?.scrollTo(0, scrollElement.scrollHeight)
|
||||
afterScrollFn?.()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function scrollToSeparator (afterScrollFn?: () => void) {
|
||||
setTimeout(() => {
|
||||
if (separatorElement) {
|
||||
separatorElement.scrollIntoView()
|
||||
afterScrollFn?.()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function handleJumpToDate (e: CustomEvent) {
|
||||
const date = e.detail.date
|
||||
|
||||
if (!date || !scrollElement) {
|
||||
return
|
||||
}
|
||||
|
||||
let closestDate = getClosestDateSelectorDate(date, scrollElement)
|
||||
|
||||
if (!closestDate) {
|
||||
return
|
||||
}
|
||||
|
||||
if (closestDate < date) {
|
||||
closestDate = undefined
|
||||
} else {
|
||||
scrollToDate(closestDate)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToDate (date: Timestamp) {
|
||||
let offset = date && document.getElementById(date.toString())?.offsetTop
|
||||
|
||||
if (!offset || !scrollElement) {
|
||||
return
|
||||
}
|
||||
|
||||
offset = offset - headerHeight - dateSelectorHeight / 2
|
||||
|
||||
scrollElement.scrollTo({ left: 0, top: offset })
|
||||
}
|
||||
|
||||
function handleScroll () {
|
||||
updateSelectedDate()
|
||||
readViewportMessages()
|
||||
}
|
||||
|
||||
function readViewportMessages () {
|
||||
if (!isViewportInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
const containerRect = scrollElement?.getBoundingClientRect()
|
||||
|
||||
if (containerRect === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const messagesToRead: DisplayActivityMessage[] = []
|
||||
|
||||
for (const message of displayMessages) {
|
||||
const msgElement = document.getElementById(message._id)
|
||||
|
||||
if (!msgElement) {
|
||||
continue
|
||||
}
|
||||
|
||||
const messageRect = msgElement.getBoundingClientRect()
|
||||
|
||||
if (messageRect.top >= containerRect.top && messageRect.bottom - messageRect.height / 2 <= containerRect.bottom) {
|
||||
messagesToRead.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
readMessage(messagesToRead)
|
||||
}
|
||||
|
||||
function readMessage (messages: DisplayActivityMessage[]) {
|
||||
if (inboxClient === undefined || messages.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const allIds = messages
|
||||
.map((message) => {
|
||||
const combined =
|
||||
message._class === activity.class.DocUpdateMessage
|
||||
? (message as DisplayDocUpdateMessage)?.combinedMessagesIds
|
||||
: undefined
|
||||
|
||||
return [message._id, ...(combined ?? [])]
|
||||
})
|
||||
.flat()
|
||||
|
||||
inboxClient.readMessages(allIds)
|
||||
|
||||
const notifyContext = get(inboxClient.docNotifyContextByDoc).get(object._id)
|
||||
const lastTimestamp = messages[messages.length - 1].createdOn ?? 0
|
||||
|
||||
if (notifyContext !== undefined && (notifyContext.lastViewedTimestamp ?? 0) < lastTimestamp) {
|
||||
client.update(notifyContext, { lastViewedTimestamp: lastTimestamp })
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectedDate () {
|
||||
if (!scrollElement) {
|
||||
return
|
||||
}
|
||||
|
||||
const clientRect = scrollElement.getBoundingClientRect()
|
||||
const dateSelectors = scrollElement.getElementsByClassName('dateSelector')
|
||||
const firstVisibleDateElement = Array.from(dateSelectors)
|
||||
.reverse()
|
||||
.find((child) => {
|
||||
if (child?.nodeType === Node.ELEMENT_NODE) {
|
||||
const rect = child?.getBoundingClientRect()
|
||||
if (rect.top - dateSelectorHeight / 2 <= clientRect.top + dateSelectorHeight) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (!firstVisibleDateElement) {
|
||||
return
|
||||
}
|
||||
|
||||
showDateSelector = clientRect.top - firstVisibleDateElement.getBoundingClientRect().top > -dateSelectorHeight / 2
|
||||
selectedDate = parseInt(firstVisibleDateElement.id)
|
||||
}
|
||||
|
||||
$: filterActivityMessages(messages, filters, object._class, filter).then((filteredMessages) => {
|
||||
displayMessages = filteredMessages
|
||||
})
|
||||
|
||||
function getNewPosition (displayMessages: ActivityMessage[], lastViewedTimestamp?: Timestamp): number | undefined {
|
||||
if (displayMessages.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (separatorPosition !== undefined) {
|
||||
return separatorPosition
|
||||
}
|
||||
|
||||
if (lastViewedTimestamp === undefined) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (lastViewedTimestamp === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const lastViewedMessageIdx = displayMessages.findIndex((message, index) => {
|
||||
const createdOn = message.createdOn ?? 0
|
||||
const nextCreatedOn = displayMessages[index + 1]?.createdOn ?? 0
|
||||
|
||||
return lastViewedTimestamp >= createdOn && lastViewedTimestamp < nextCreatedOn
|
||||
})
|
||||
|
||||
return lastViewedMessageIdx !== -1 ? lastViewedMessageIdx + 1 : -1
|
||||
}
|
||||
|
||||
$: separatorPosition = getNewPosition(displayMessages, lastViewedTimestamp)
|
||||
$: initializeViewport(scrollElement, separatorElement, separatorPosition)
|
||||
|
||||
function initializeViewport (
|
||||
scrollElement?: HTMLDivElement,
|
||||
separatorElement?: HTMLDivElement,
|
||||
separatorPosition?: number
|
||||
) {
|
||||
if (separatorPosition === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (separatorPosition < 0 && scrollElement) {
|
||||
scrollToBottom(markViewportInitialized)
|
||||
} else if (separatorElement) {
|
||||
scrollToSeparator(markViewportInitialized)
|
||||
}
|
||||
}
|
||||
|
||||
function markViewportInitialized () {
|
||||
// We should mark viewport as initialized when scroll is finished
|
||||
setTimeout(() => {
|
||||
isViewportInitialized = true
|
||||
readViewportMessages()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function handleMessageSent () {
|
||||
scrollToBottom(markViewportInitialized)
|
||||
}
|
||||
|
||||
$: if (isViewportInitialized && messages.length > prevMessagesLength) {
|
||||
setTimeout(() => {
|
||||
readViewportMessages()
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !isLoading}
|
||||
<div class="flex-col vScroll" bind:this={scrollElement} on:scroll={handleScroll}>
|
||||
{#if startFromBottom}
|
||||
<div class="grower" />
|
||||
{/if}
|
||||
{#if showDateSelector}
|
||||
<div class="ml-2 pr-2 fixed">
|
||||
<JumpToDateSelector {selectedDate} fixed on:jumpToDate={handleJumpToDate} />
|
||||
</div>
|
||||
{/if}
|
||||
{#each displayMessages as message, index}
|
||||
{#if index === separatorPosition}
|
||||
<ActivityMessagesSeparator bind:element={separatorElement} title={activity.string.New} line reverse isNew />
|
||||
{/if}
|
||||
|
||||
{#if withDates && (index === 0 || isOtherDay(message.createdOn ?? 0, displayMessages[index - 1].createdOn ?? 0))}
|
||||
<JumpToDateSelector selectedDate={message.createdOn} on:jumpToDate={handleJumpToDate} />
|
||||
{/if}
|
||||
|
||||
<ActivityMessagePresenter
|
||||
value={message}
|
||||
skipLabel={skipLabels}
|
||||
{showEmbedded}
|
||||
isHighlighted={message._id === selectedMessageId}
|
||||
shouldScroll={selectedMessageId === message._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ref-input">
|
||||
<ActivityExtensionComponent
|
||||
kind="input"
|
||||
{extensions}
|
||||
props={{ object, boundary: scrollElement, collection }}
|
||||
on:submit={handleMessageSent}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.grower {
|
||||
flex-grow: 10;
|
||||
flex-shrink: 5;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: absolute;
|
||||
align-self: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ref-input {
|
||||
margin: 1.25rem 1rem;
|
||||
}
|
||||
</style>
|
@ -15,15 +15,17 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Label, resizeObserver, CheckBox, MiniToggle } from '@hcengineering/ui'
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { CheckBox, Label, MiniToggle, resizeObserver } from '@hcengineering/ui'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { ActivityMessagesFilter } from '@hcengineering/activity'
|
||||
|
||||
import activity from '../plugin'
|
||||
|
||||
export let selectedFiltersRefs: Ref<Doc>[] | 'All' = 'All'
|
||||
export let selectedFiltersRefs: Ref<ActivityMessagesFilter>[] | Ref<ActivityMessagesFilter> = activity.ids.AllFilter
|
||||
export let filters: ActivityMessagesFilter[] = []
|
||||
|
||||
const allId = activity.ids.AllFilter
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
|
||||
@ -31,20 +33,19 @@
|
||||
interface ActionMenu {
|
||||
label: IntlString
|
||||
checked: boolean
|
||||
value: Ref<Doc> | 'All'
|
||||
value: Ref<ActivityMessagesFilter>
|
||||
}
|
||||
let menu: ActionMenu[] = [
|
||||
{
|
||||
label: activity.string.All,
|
||||
checked: true,
|
||||
value: 'All'
|
||||
}
|
||||
]
|
||||
filters.map((fl) => menu.push({ label: fl.label, checked: false, value: fl._id }))
|
||||
if (selectedFiltersRefs !== 'All') {
|
||||
selectedFiltersRefs.forEach((fl) => {
|
||||
const index = menu.findIndex((el) => el.value === fl)
|
||||
if (index !== -1) menu[index].checked = true
|
||||
|
||||
let menu: ActionMenu[] = []
|
||||
|
||||
filters.map(({ label, _id }) => menu.push({ label, checked: _id === allId, value: _id }))
|
||||
|
||||
if (Array.isArray(selectedFiltersRefs)) {
|
||||
selectedFiltersRefs.forEach((filterId) => {
|
||||
const index = menu.findIndex(({ value }) => value === filterId)
|
||||
if (index !== -1) {
|
||||
menu[index].checked = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -79,31 +80,31 @@
|
||||
|
||||
const checkAll = () => {
|
||||
menu.forEach((el, i) => (el.checked = i === 0))
|
||||
selectedFiltersRefs = 'All'
|
||||
selectedFiltersRefs = allId
|
||||
}
|
||||
|
||||
const uncheckAll = () => {
|
||||
menu.forEach((el) => (el.checked = true))
|
||||
const temp = filters.map((fl) => fl._id as Ref<Doc>)
|
||||
selectedFiltersRefs = temp
|
||||
selectedFiltersRefs = filters.map(({ _id }) => _id)
|
||||
}
|
||||
|
||||
const selectRow = (n: number) => {
|
||||
if (n === 0) {
|
||||
if (selectedFiltersRefs === 'All') uncheckAll()
|
||||
if (selectedFiltersRefs === allId) uncheckAll()
|
||||
else checkAll()
|
||||
} else {
|
||||
if (selectedFiltersRefs === 'All') {
|
||||
if (selectedFiltersRefs === allId) {
|
||||
menu[n].checked = true
|
||||
selectedFiltersRefs = [menu[n].value as Ref<Doc>]
|
||||
selectedFiltersRefs = [menu[n].value]
|
||||
} else if (menu[n].checked) {
|
||||
if (menu.filter((el) => el.checked).length === 2) checkAll()
|
||||
else {
|
||||
else if (Array.isArray(selectedFiltersRefs)) {
|
||||
menu[n].checked = false
|
||||
selectedFiltersRefs = selectedFiltersRefs.filter((fl) => fl !== menu[n].value)
|
||||
}
|
||||
} else {
|
||||
} else if (Array.isArray(selectedFiltersRefs)) {
|
||||
menu[n].checked = true
|
||||
selectedFiltersRefs.push(menu[n].value as Ref<Doc>)
|
||||
selectedFiltersRefs.push(menu[n].value)
|
||||
}
|
||||
}
|
||||
menu = menu
|
||||
@ -152,7 +153,7 @@
|
||||
}}
|
||||
>
|
||||
<div class="flex-center justify-end mr-3 pointer-events-none">
|
||||
<CheckBox checked={item.checked} symbol={selectedFiltersRefs !== 'All' && i === 0 ? 'minus' : 'check'} />
|
||||
<CheckBox checked={item.checked} symbol={selectedFiltersRefs !== allId && i === 0 ? 'minus' : 'check'} />
|
||||
</div>
|
||||
<span class="overflow-label">
|
||||
<Label label={item.label} />
|
||||
|
@ -1,8 +1,21 @@
|
||||
<!--
|
||||
// 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 { Timestamp } from '@hcengineering/core'
|
||||
import { getDay, Timestamp } from '@hcengineering/core'
|
||||
import { DateRangePopup, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getDay } from '../utils'
|
||||
|
||||
export let selectedDate: Timestamp | undefined
|
||||
export let fixed: boolean = false
|
||||
@ -17,7 +30,6 @@
|
||||
|
||||
<div id={fixed ? '' : time?.toString()} class="flex-center clear-mins dateSelector">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={div}
|
||||
class="border-radius-4 over-underline dateSelectorButton clear-mins"
|
167
plugins/activity-resources/src/components/Replies.svelte
Normal file
167
plugins/activity-resources/src/components/Replies.svelte
Normal file
@ -0,0 +1,167 @@
|
||||
<!--
|
||||
// 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 { Person } from '@hcengineering/contact'
|
||||
import { personByIdStore, Avatar } from '@hcengineering/contact-resources'
|
||||
import { Doc, IdMap, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { getLocation, Label, navigate, TimeSince } from '@hcengineering/ui'
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
import notification, {
|
||||
ActivityInboxNotification,
|
||||
DocNotifyContext,
|
||||
InboxNotification,
|
||||
InboxNotificationsClient
|
||||
} from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
import activity from '../plugin'
|
||||
|
||||
export let message: ActivityMessage
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const maxDisplayPersons = 5
|
||||
|
||||
$: lastReply = message.lastReply ?? new Date().getTime()
|
||||
$: persons = new Set(message.repliedPersons)
|
||||
|
||||
let inboxClient: InboxNotificationsClient | undefined = undefined
|
||||
|
||||
getResource(notification.function.GetInboxNotificationsClient).then((getClientFn) => {
|
||||
inboxClient = getClientFn()
|
||||
})
|
||||
|
||||
let displayPersons: Person[] = []
|
||||
|
||||
$: docNotifyContextByDocStore = inboxClient?.docNotifyContextByDoc
|
||||
$: inboxNotificationsByContextStore = inboxClient?.inboxNotificationsByContext
|
||||
|
||||
$: hasNew = hasNewReplies(message, $docNotifyContextByDocStore, $inboxNotificationsByContextStore)
|
||||
$: updateQuery(persons, $personByIdStore)
|
||||
|
||||
function hasNewReplies (
|
||||
message: ActivityMessage,
|
||||
notifyContexts?: Map<Ref<Doc>, DocNotifyContext>,
|
||||
inboxNotificationsByContext?: Map<Ref<DocNotifyContext>, WithLookup<InboxNotification>[]>
|
||||
): boolean {
|
||||
const context: DocNotifyContext | undefined = notifyContexts?.get(message._id)
|
||||
|
||||
if (context === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (inboxNotificationsByContext?.get(context._id) ?? [])
|
||||
.filter((notification) => {
|
||||
const activityNotifications = notification as ActivityInboxNotification
|
||||
return activityNotifications.attachedToClass !== activity.class.DocUpdateMessage
|
||||
})
|
||||
.some(({ isViewed }) => !isViewed)
|
||||
}
|
||||
|
||||
function updateQuery (personIds: Set<Ref<Person>>, personById: IdMap<Person>) {
|
||||
displayPersons = Array.from(personIds)
|
||||
.map((id) => personById.get(id))
|
||||
.filter((person): person is Person => person !== undefined)
|
||||
.slice(0, maxDisplayPersons - 1)
|
||||
}
|
||||
|
||||
function handleReply (e: any) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
if (onReply) {
|
||||
onReply()
|
||||
return
|
||||
}
|
||||
const loc = getLocation()
|
||||
loc.path[4] = message._id
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex-row-center container cursor-pointer" on:click={handleReply}>
|
||||
<div class="flex-row-center">
|
||||
<div class="avatars">
|
||||
{#each displayPersons as person}
|
||||
<Avatar size="x-small" avatar={person.avatar} name={person.name} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if persons.size > maxDisplayPersons}
|
||||
<div class="plus">
|
||||
+{persons.size - maxDisplayPersons}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="whitespace-nowrap ml-2 mr-2 over-underline repliesCount">
|
||||
<Label label={activity.string.RepliesCount} params={{ replies: message.replies ?? 0 }} />
|
||||
</div>
|
||||
{#if hasNew}
|
||||
<div class="notifyMarker" />
|
||||
{/if}
|
||||
<div class="lastReply">
|
||||
<Label label={activity.string.LastReply} />
|
||||
</div>
|
||||
<div class="time">
|
||||
<TimeSince value={lastReply} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
width: fit-content;
|
||||
margin-left: -0.5rem;
|
||||
|
||||
.plus {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.repliesCount {
|
||||
color: var(--theme-link-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.lastReply {
|
||||
font-size: 0.75rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.notifyMarker {
|
||||
margin-right: 0.25rem;
|
||||
width: 0.425rem;
|
||||
height: 0.425rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--highlight-red);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--button-border-hover);
|
||||
background-color: var(--theme-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,51 @@
|
||||
<!--
|
||||
// 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 { ActionIcon } from '@hcengineering/ui'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, { ActivityMessage, SavedMessage } from '@hcengineering/activity'
|
||||
import preference from '@hcengineering/preference'
|
||||
|
||||
import Bookmark from './icons/Bookmark.svelte'
|
||||
|
||||
export let object: ActivityMessage
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
|
||||
let savedMessage: SavedMessage | undefined = undefined
|
||||
|
||||
$: query.query(activity.class.SavedMessage, { attachedTo: object._id }, (res) => {
|
||||
savedMessage = res[0]
|
||||
})
|
||||
|
||||
async function toggleSaveMessage (): Promise<void> {
|
||||
if (savedMessage !== undefined) {
|
||||
await client.remove(savedMessage)
|
||||
savedMessage = undefined
|
||||
} else {
|
||||
await client.createDoc(activity.class.SavedMessage, preference.space.Preference, {
|
||||
attachedTo: object._id
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionIcon
|
||||
icon={Bookmark}
|
||||
iconProps={savedMessage ? { fill: '#3265cb' } : undefined}
|
||||
size="medium"
|
||||
action={toggleSaveMessage}
|
||||
/>
|
@ -36,7 +36,7 @@
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let withActions: boolean = true
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
$: personAccount = $personAccountByIdStore.get((value.createdBy ?? value.modifiedBy) as Ref<PersonAccount>)
|
||||
@ -65,7 +65,7 @@
|
||||
{isSelected}
|
||||
{shouldScroll}
|
||||
{embedded}
|
||||
{hasActionsMenu}
|
||||
{withActions}
|
||||
viewlet={undefined}
|
||||
{onClick}
|
||||
>
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import view from '@hcengineering/view'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import { Action, Component } from '@hcengineering/ui'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
|
||||
export let value: DisplayActivityMessage
|
||||
@ -25,8 +25,14 @@
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let withActions: boolean = true
|
||||
export let showEmbedded = false
|
||||
export let hideReplies = false
|
||||
export let skipLabel = false
|
||||
export let actions: Action[] = []
|
||||
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -40,12 +46,17 @@
|
||||
props={{
|
||||
value,
|
||||
showNotify,
|
||||
skipLabel,
|
||||
isHighlighted,
|
||||
isSelected,
|
||||
shouldScroll,
|
||||
embedded,
|
||||
hasActionsMenu,
|
||||
onClick
|
||||
withActions,
|
||||
showEmbedded,
|
||||
hideReplies,
|
||||
actions,
|
||||
onClick,
|
||||
onReply
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -30,6 +30,8 @@
|
||||
import ActivityMessageExtensionComponent from './ActivityMessageExtension.svelte'
|
||||
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
|
||||
import PinMessageAction from './PinMessageAction.svelte'
|
||||
import Replies from '../Replies.svelte'
|
||||
import SaveMessageAction from '../SaveMessageAction.svelte'
|
||||
|
||||
export let message: DisplayActivityMessage
|
||||
export let parentMessage: DisplayActivityMessage | undefined
|
||||
@ -43,8 +45,11 @@
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let withActions: boolean = true
|
||||
export let showEmbedded = false
|
||||
export let hideReplies = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
let allActionIds: string[] = []
|
||||
@ -53,9 +58,10 @@
|
||||
let extensions: ActivityMessageExtension[] = []
|
||||
let isActionMenuOpened = false
|
||||
|
||||
$: void getActions(client, message, activity.class.ActivityMessage).then((res) => {
|
||||
allActionIds = res.map(({ _id }) => _id)
|
||||
})
|
||||
$: withActions &&
|
||||
getActions(client, message, activity.class.ActivityMessage).then((res) => {
|
||||
allActionIds = res.map(({ _id }) => _id)
|
||||
})
|
||||
|
||||
function scrollToMessage (): void {
|
||||
if (element != null && shouldScroll) {
|
||||
@ -101,7 +107,7 @@
|
||||
|
||||
$: isHidden = !!viewlet?.onlyWithParent && parentMessage === undefined
|
||||
$: withActionMenu =
|
||||
!embedded && hasActionsMenu && (actions.length > 0 || allActionIds.some((id) => !excludedActions.includes(id)))
|
||||
withActions && !embedded && (actions.length > 0 || allActionIds.some((id) => !excludedActions.includes(id)))
|
||||
</script>
|
||||
|
||||
{#if !isHidden}
|
||||
@ -151,32 +157,38 @@
|
||||
|
||||
<slot name="content" />
|
||||
|
||||
{#if !hideReplies && message.replies && message.replies > 0}
|
||||
<div class="mt-2" />
|
||||
<Replies {message} {onReply} />
|
||||
{/if}
|
||||
<ActivityMessageExtensionComponent kind="footer" {extensions} props={{ object: message }} />
|
||||
|
||||
<ReactionsPresenter object={message} />
|
||||
|
||||
{#if parentMessage}
|
||||
{#if parentMessage && showEmbedded}
|
||||
<div class="mt-2" />
|
||||
<ActivityMessagePresenter value={parentMessage} embedded />
|
||||
<ActivityMessagePresenter value={parentMessage} embedded hideReplies withActions={false} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actions clear-mins flex flex-gap-2 items-center"
|
||||
class:menuShowed={isActionMenuOpened || message.isPinned}
|
||||
class:opened={isActionMenuOpened || message.isPinned}
|
||||
>
|
||||
<AddReactionAction object={message} />
|
||||
<PinMessageAction object={message} />
|
||||
{#if withActions}
|
||||
<AddReactionAction object={message} />
|
||||
<PinMessageAction object={message} />
|
||||
<SaveMessageAction object={message} />
|
||||
|
||||
<ActivityMessageExtensionComponent
|
||||
kind="action"
|
||||
{extensions}
|
||||
props={{ object: message }}
|
||||
on:close={handleActionMenuClosed}
|
||||
on:open={handleActionMenuOpened}
|
||||
/>
|
||||
{#if withActionMenu}
|
||||
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
|
||||
<ActivityMessageExtensionComponent
|
||||
kind="action"
|
||||
{extensions}
|
||||
props={{ object: message }}
|
||||
on:close={handleActionMenuClosed}
|
||||
on:open={handleActionMenuOpened}
|
||||
/>
|
||||
{#if withActionMenu}
|
||||
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@ -195,8 +207,8 @@
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 0.75rem 0.75rem 0.75rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
@ -228,7 +240,7 @@
|
||||
right: 0.75rem;
|
||||
color: var(--theme-halfcontent-color);
|
||||
|
||||
&.menuShowed {
|
||||
&.opened {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
// 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
|
||||
@ -21,9 +21,15 @@
|
||||
export let params: any = undefined
|
||||
export let reverse: boolean = false
|
||||
export let isNew: boolean = false
|
||||
export let element: HTMLDivElement | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="w-full text-sm flex-center whitespace-nowrap" class:flex-reverse={reverse} class:new={isNew}>
|
||||
<div
|
||||
class="w-full text-sm flex-center whitespace-nowrap"
|
||||
class:flex-reverse={reverse}
|
||||
class:new={isNew}
|
||||
bind:this={element}
|
||||
>
|
||||
<div class:ml-8={!reverse} class:mr-4={reverse}><Label label={title} {params} /></div>
|
||||
<div class:ml-4={!reverse} class:mr-4={reverse} class:line />
|
||||
</div>
|
||||
@ -35,6 +41,7 @@
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.new {
|
||||
.line {
|
||||
background-color: var(--highlight-red);
|
@ -25,7 +25,7 @@
|
||||
import core, { Account, AttachedDoc, Class, Collection, Doc, Ref } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Component, ShowMore } from '@hcengineering/ui'
|
||||
import { Component, ShowMore, Action } from '@hcengineering/ui'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
|
||||
import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte'
|
||||
@ -42,8 +42,12 @@
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let withActions: boolean = true
|
||||
export let showEmbedded = false
|
||||
export let hideReplies = false
|
||||
export let actions: Action[] = []
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -146,9 +150,13 @@
|
||||
{isSelected}
|
||||
{shouldScroll}
|
||||
{embedded}
|
||||
{hasActionsMenu}
|
||||
{withActions}
|
||||
{viewlet}
|
||||
{showEmbedded}
|
||||
{hideReplies}
|
||||
{actions}
|
||||
{onClick}
|
||||
{onReply}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
{#if viewlet?.labelComponent}
|
||||
|
@ -0,0 +1,24 @@
|
||||
<!--
|
||||
// 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.2,18c-0.1,0-0.2,0-0.3-0.1l-6-4l-6,4c-0.2,0.1-0.4,0.1-0.5,0c-0.2-0.1-0.3-0.3-0.3-0.4V4.2 C3.2,3,4.3,2,5.5,2h8.9c1.3,0,2.3,1,2.3,2.2v13.3c0,0.2-0.1,0.4-0.3,0.4C16.4,18,16.3,18,16.2,18z M10,12.8c0.1,0,0.2,0,0.3,0.1 l5.5,3.6V4.2c0-0.6-0.6-1.2-1.3-1.2H5.5C4.8,3,4.2,3.5,4.2,4.2v12.4l5.5-3.6C9.8,12.9,9.9,12.8,10,12.8z"
|
||||
/>
|
||||
</svg>
|
@ -16,12 +16,11 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { ActionIcon, EmojiPopup, IconEmoji, showPopup } from '@hcengineering/ui'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, { Reaction } from '@hcengineering/activity'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
|
||||
|
||||
import { updateDocReactions } from '../../utils'
|
||||
|
||||
export let object: Doc | undefined = undefined
|
||||
export let object: ActivityMessage | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@ -38,16 +37,10 @@
|
||||
|
||||
function openEmojiPalette (ev: Event) {
|
||||
dispatch('open')
|
||||
showPopup(
|
||||
EmojiPopup,
|
||||
{},
|
||||
ev.target as HTMLElement,
|
||||
(emoji: string) => {
|
||||
updateDocReactions(client, reactions, object, emoji)
|
||||
dispatch('close')
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
showPopup(EmojiPopup, {}, ev.target as HTMLElement, (emoji: string) => {
|
||||
updateDocReactions(client, reactions, object, emoji)
|
||||
dispatch('close')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -14,13 +14,12 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, { Reaction } from '@hcengineering/activity'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
|
||||
|
||||
import Reactions from './Reactions.svelte'
|
||||
import { updateDocReactions } from '../../utils'
|
||||
|
||||
export let object: Doc | undefined
|
||||
export let object: ActivityMessage | undefined
|
||||
|
||||
const client = getClient()
|
||||
const reactionsQuery = createQuery()
|
||||
@ -38,7 +37,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if reactions.length}
|
||||
{#if object !== undefined && reactions.length > 0}
|
||||
<div class="footer flex-col p-inline contrast mt-2">
|
||||
<Reactions {reactions} {object} on:click={handleClick} />
|
||||
</div>
|
||||
|
@ -21,7 +21,7 @@ import DocUpdateMessagePresenter from './components/doc-update-message/DocUpdate
|
||||
import ActivityInfoMessagePresenter from './components/activity-message/ActivityInfoMessagePresenter.svelte'
|
||||
import ReactionAddedMessage from './components/reactions/ReactionAddedMessage.svelte'
|
||||
|
||||
import { attributesFilter, pinnedFilter } from './activityMessagesUtils'
|
||||
import { getMessageFragment, attributesFilter, pinnedFilter, allFilter } from './activityMessagesUtils'
|
||||
|
||||
export * from './activity'
|
||||
export * from './utils'
|
||||
@ -31,6 +31,7 @@ export { default as Reactions } from './components/reactions/Reactions.svelte'
|
||||
export { default as ActivityMessageTemplate } from './components/activity-message/ActivityMessageTemplate.svelte'
|
||||
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 async (): Promise<Resources> => ({
|
||||
@ -43,6 +44,10 @@ export default async (): Promise<Resources> => ({
|
||||
},
|
||||
filter: {
|
||||
AttributesFilter: attributesFilter,
|
||||
PinnedFilter: pinnedFilter
|
||||
PinnedFilter: pinnedFilter,
|
||||
AllFilter: allFilter
|
||||
},
|
||||
function: {
|
||||
GetFragment: getMessageFragment
|
||||
}
|
||||
})
|
||||
|
@ -29,8 +29,11 @@
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/ui": "^0.6.11"
|
||||
"@hcengineering/preference": "^0.6.9",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/view": "^0.6.9"
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import {
|
||||
Account,
|
||||
AttachedDoc,
|
||||
@ -32,6 +33,7 @@ import {
|
||||
} from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import { Preference } from '@hcengineering/preference'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
|
||||
// TODO: remove TxViewlet
|
||||
@ -110,10 +112,14 @@ export interface ActivityMessage extends AttachedDoc {
|
||||
|
||||
isPinned?: boolean
|
||||
|
||||
repliedPersons?: Ref<Person>[]
|
||||
lastReply?: Timestamp
|
||||
|
||||
replies?: number
|
||||
reactions?: number
|
||||
}
|
||||
|
||||
export type DisplayActivityMessage = DisplayDocUpdateMessage | ActivityMessage | ActivityInfoMessage
|
||||
export type DisplayActivityMessage = DisplayDocUpdateMessage | ActivityMessage
|
||||
|
||||
export interface DisplayDocUpdateMessage extends DocUpdateMessage {
|
||||
previousMessages?: DocUpdateMessage[]
|
||||
@ -228,6 +234,7 @@ export const activityId = 'activity' as Plugin
|
||||
*/
|
||||
export interface ActivityMessagesFilter extends Doc {
|
||||
label: IntlString
|
||||
position: number
|
||||
filter: Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||
}
|
||||
|
||||
@ -262,10 +269,19 @@ export interface ActivityExtension extends Doc {
|
||||
* @public
|
||||
*/
|
||||
export interface Reaction extends AttachedDoc {
|
||||
attachedTo: Ref<ActivityMessage>
|
||||
attachedToClass: Ref<Class<ActivityMessage>>
|
||||
emoji: string
|
||||
createBy: Ref<Account>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SavedMessage extends Preference {
|
||||
attachedTo: Ref<ActivityMessage>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -287,11 +303,13 @@ export default plugin(activityId, {
|
||||
ActivityMessageExtension: '' as Ref<Class<ActivityMessageExtension>>,
|
||||
ActivityMessagesFilter: '' as Ref<Class<ActivityMessagesFilter>>,
|
||||
ActivityExtension: '' as Ref<Class<ActivityExtension>>,
|
||||
Reaction: '' as Ref<Class<Reaction>>
|
||||
Reaction: '' as Ref<Class<Reaction>>,
|
||||
SavedMessage: '' as Ref<Class<SavedMessage>>
|
||||
},
|
||||
icon: {
|
||||
Activity: '' as Asset,
|
||||
Emoji: '' as Asset
|
||||
Emoji: '' as Asset,
|
||||
Bookmark: '' as Asset
|
||||
},
|
||||
string: {
|
||||
Activity: '' as IntlString,
|
||||
@ -313,7 +331,9 @@ export default plugin(activityId, {
|
||||
Update: '' as IntlString,
|
||||
For: '' as IntlString,
|
||||
AllActivity: '' as IntlString,
|
||||
Reactions: '' as IntlString
|
||||
Reactions: '' as IntlString,
|
||||
LastReply: '' as IntlString,
|
||||
RepliesCount: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
Activity: '' as AnyComponent,
|
||||
@ -321,5 +341,8 @@ export default plugin(activityId, {
|
||||
DocUpdateMessagePresenter: '' as AnyComponent,
|
||||
ActivityInfoMessagePresenter: '' as AnyComponent,
|
||||
ReactionAddedMessage: '' as AnyComponent
|
||||
},
|
||||
ids: {
|
||||
AllFilter: '' as Ref<ActivityMessagesFilter>
|
||||
}
|
||||
})
|
||||
|
@ -14,14 +14,18 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import type { Doc } from '@hcengineering/core'
|
||||
import type { Doc, Ref } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
|
||||
import attachment from '../plugin'
|
||||
import AttachmentList from './AttachmentList.svelte'
|
||||
|
||||
export let value: Doc & { attachments?: number }
|
||||
|
||||
const query = createQuery()
|
||||
const savedAttachmentsQuery = createQuery()
|
||||
|
||||
let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||
let attachments: Attachment[] = []
|
||||
|
||||
$: updateQuery(value)
|
||||
@ -41,6 +45,10 @@
|
||||
attachments = []
|
||||
}
|
||||
}
|
||||
|
||||
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
|
||||
savedAttachmentsIds = res.map(({ attachedTo }) => attachedTo)
|
||||
})
|
||||
</script>
|
||||
|
||||
<AttachmentList {attachments} />
|
||||
<AttachmentList {attachments} {savedAttachmentsIds} />
|
||||
|
@ -24,7 +24,6 @@ export default mergeIds(attachmentId, attachment, {
|
||||
NoAttachments: '' as IntlString,
|
||||
UploadDropFilesHere: '' as IntlString,
|
||||
Photos: '' as IntlString,
|
||||
FileBrowser: '' as IntlString,
|
||||
FileBrowserFileCounter: '' as IntlString,
|
||||
FileBrowserListView: '' as IntlString,
|
||||
FileBrowserGridView: '' as IntlString,
|
||||
|
@ -56,7 +56,8 @@ export default plugin(attachmentId, {
|
||||
component: {
|
||||
Attachments: '' as AnyComponent,
|
||||
Photos: '' as AnyComponent,
|
||||
AttachmentsPresenter: '' as AnyComponent
|
||||
AttachmentsPresenter: '' as AnyComponent,
|
||||
FileBrowser: '' as AnyComponent
|
||||
},
|
||||
icon: {
|
||||
Attachment: '' as Asset,
|
||||
@ -89,6 +90,7 @@ export default plugin(attachmentId, {
|
||||
FileBrowserTypeFilterVideos: '' as IntlString,
|
||||
FileBrowserTypeFilterPDFs: '' as IntlString,
|
||||
DeleteFile: '' as IntlString,
|
||||
Attachments: '' as IntlString
|
||||
Attachments: '' as IntlString,
|
||||
FileBrowser: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -15,14 +15,14 @@
|
||||
<script lang="ts">
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { NotificationClientImpl } from '@hcengineering/notification-resources'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { Icon, IconSize } from '@hcengineering/ui'
|
||||
|
||||
export let object: Doc
|
||||
export let size: IconSize = 'small'
|
||||
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const store = notificationClient.docUpdatesStore
|
||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||
const store = inboxClient.docNotifyContextByDoc
|
||||
$: subscribed = $store.get(object._id) !== undefined
|
||||
</script>
|
||||
|
||||
|
@ -11,9 +11,6 @@
|
||||
<symbol id="thread" viewBox="0 0 24 24">
|
||||
<path d="M22.8,10.2c0-5-4.3-9-9.6-9s-9.6,4-9.6,9c0,0.1,0,0.3,0,0.4c-1.5,1.1-2.4,2.8-2.4,4.6c0,1.7,0.8,3.3,2.2,4.4 l-0.2,1.5c-0.1,0.5,0.2,1,0.6,1.3c0.2,0.1,0.5,0.2,0.7,0.2c0.2,0,0.5-0.1,0.7-0.2l2.4-1.4c1.7-0.1,3.3-0.8,4.4-1.9 c0.3,0,0.6,0.1,0.8,0.1l4,2.4c0.3,0.2,0.6,0.3,0.9,0.3c0.3,0,0.7-0.1,0.9-0.3c0.6-0.4,0.9-1,0.8-1.7l-0.3-2.7 C21.5,15.5,22.8,13,22.8,10.2z M7.5,19.6c-0.1,0-0.3,0-0.4,0.1l-2.4,1.4l0.2-1.7c0-0.3-0.1-0.6-0.3-0.7c-1.2-0.8-1.9-2.1-1.9-3.5 c0-1.4,0.8-2.8,2-3.6c0.8-0.5,1.7-0.8,2.7-0.8c2.3,0,4.2,1.5,4.7,3.5c0.1,0.3,0.1,0.6,0.1,0.9c0,0.2,0,0.5-0.1,0.7c0,0,0,0.1,0,0.1 c-0.1,0.7-0.4,1.3-0.9,1.9C10.4,19,9,19.6,7.5,19.6z M18,16.2c-0.2,0.2-0.3,0.4-0.3,0.7l0.4,3.2c0,0.1-0.1,0.2-0.1,0.2 c-0.1,0-0.1,0.1-0.3,0l-4.2-2.5c-0.1-0.1-0.3-0.1-0.4-0.1c0.1-0.2,0.2-0.3,0.2-0.5c0,0,0,0,0-0.1c0.1-0.3,0.2-0.6,0.2-0.8 c0-0.1,0-0.1,0-0.2c0-0.3,0.1-0.6,0.1-0.9c0-0.2,0-0.4,0-0.6c-0.3-3-3-5.3-6.2-5.3c-0.3,0-0.6,0-0.8,0.1c-0.1,0-0.1,0-0.2,0 c-0.3,0-0.5,0.1-0.8,0.2c0,0,0,0-0.1,0C5.4,9.7,5.3,9.7,5.1,9.8c0.3-3.9,3.8-7,8.1-7c4.5,0,8.1,3.4,8.1,7.5 C21.2,12.6,20.1,14.8,18,16.2z" />
|
||||
</symbol>
|
||||
<symbol id="bookmark" viewBox="0 0 24 24">
|
||||
<path d="M20,4.2h-2.2V2c0-0.4-0.3-0.8-0.8-0.8H4C3.6,1.2,3.2,1.6,3.2,2v17c0,0.3,0.1,0.5,0.4,0.6c0.2,0.1,0.5,0.1,0.7,0 l1.9-1V22c0,0.3,0.1,0.5,0.4,0.6c0.2,0.1,0.5,0.2,0.7,0l6.2-3l6.2,3c0.2,0.1,0.5,0.1,0.7,0c0.2-0.1,0.4-0.4,0.4-0.6V5 C20.8,4.6,20.4,4.2,20,4.2z M4.8,17.8v-15h11.5v1.5H7C6.6,4.2,6.2,4.6,6.2,5v12L4.8,17.8z M19.2,20.8l-5.4-2.6 c-0.2-0.1-0.4-0.1-0.7,0l-5.4,2.6V5.8h11.5V20.8z" />
|
||||
</symbol>
|
||||
<symbol id="channelbrowser" viewBox="0 0 24 24">
|
||||
<path d="M12.4,20.2H8.6c-4.3,0-5.9-1.6-5.9-5.8V8.7c0-4.4,1.5-6,5.9-6h5.7c4.4,0,6,1.6,6,6v3.7c0,0.4,0.3,0.7,0.8,0.7 s0.8-0.3,0.8-0.7V8.7c0-5.2-2.2-7.5-7.5-7.5H8.6c-5.2,0-7.4,2.2-7.4,7.5v5.7c0,5.1,2.3,7.3,7.4,7.3h3.8c0.4,0,0.7-0.3,0.7-0.8 S12.8,20.2,12.4,20.2z" />
|
||||
<path d="M22.2,21.4l-1-1c0,0,0,0-0.1,0c0.4-0.6,0.7-1.4,0.7-2.2c0-2.2-1.7-3.9-3.9-3.9s-4,1.7-4,3.9c0,2.1,1.8,4,4,4 c0.8,0,1.6-0.2,2.2-0.7c0,0,0,0,0,0.1l1,1c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2C22.5,22.1,22.5,21.7,22.2,21.4z M17.9,20.5 c-1.3,0-2.5-1.2-2.5-2.5c0-1.4,1.1-2.4,2.5-2.4s2.4,1.1,2.4,2.4S19.3,20.5,17.9,20.5z" />
|
||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.1 KiB |
@ -29,9 +29,6 @@
|
||||
"Chat": "Chat",
|
||||
"In": "In",
|
||||
"MentionNotification": "Mentioned",
|
||||
"Replies": "Replies",
|
||||
"LastReply": "Last reply",
|
||||
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
|
||||
"Topic": "Topic",
|
||||
"Thread": "Thread",
|
||||
"Threads": "Threads",
|
||||
@ -80,6 +77,13 @@
|
||||
"DirectNotificationTitle": "{senderName}",
|
||||
"DirectNotificationBody": "{message}",
|
||||
"AddCommentPlaceholder": "Add comment...",
|
||||
"Reacted": "Reacted"
|
||||
"Reacted": "Reacted",
|
||||
"Saved": "Saved",
|
||||
"Docs": "Documents",
|
||||
"NewestFirst": "Newest first",
|
||||
"ReplyToThread": "Reply to thread",
|
||||
"SentMessage": "Sent message",
|
||||
"Direct": "direct",
|
||||
"RepliedToThread": "Replied to thread"
|
||||
}
|
||||
}
|
@ -29,9 +29,6 @@
|
||||
"Reference": "Ссылка",
|
||||
"Chat": "Чат",
|
||||
"In": "в",
|
||||
"Replies": "Ответы",
|
||||
"LastReply": "Последний ответ",
|
||||
"RepliesCount": "{replies, plural, one {# ответ} few {# ответа} other {# ответов}}",
|
||||
"Topic": "Топик",
|
||||
"Thread": "Обсуждение",
|
||||
"Threads": "Обсуждения",
|
||||
@ -80,6 +77,13 @@
|
||||
"DirectNotificationTitle": "{senderName}",
|
||||
"DirectNotificationBody": "{message}",
|
||||
"AddCommentPlaceholder": "Добавить комментарий...",
|
||||
"Reacted": "Отреагировал(а)"
|
||||
"Reacted": "Отреагировал(а)",
|
||||
"Saved": "Сохранено",
|
||||
"Docs": "Documents",
|
||||
"NewestFirst": "Сначала новые",
|
||||
"ReplyToThread": "Ответить в канале",
|
||||
"SentMessage": "Отправил(а) сообщение",
|
||||
"Direct": "личные сообщения",
|
||||
"RepliedToThread": "Ответил(а) в канале"
|
||||
}
|
||||
}
|
@ -22,6 +22,5 @@ loadMetadata(chunter.icon, {
|
||||
Hashtag: `${icons}#hashtag`,
|
||||
Thread: `${icons}#thread`,
|
||||
Lock: `${icons}#lock`,
|
||||
Bookmark: `${icons}#bookmark`,
|
||||
ChannelBrowser: `${icons}#channelbrowser`
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
// 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
|
||||
@ -13,228 +13,39 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import type { ChunterMessage, Message } from '@hcengineering/chunter'
|
||||
import core, { Doc, Ref, Space, Timestamp, WithLookup } from '@hcengineering/core'
|
||||
import { DocUpdates } from '@hcengineering/notification'
|
||||
import { NotificationClientImpl } from '@hcengineering/notification-resources'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { location as locationStore } from '@hcengineering/ui'
|
||||
import { afterUpdate, beforeUpdate, onDestroy } from 'svelte'
|
||||
import activity from '@hcengineering/activity'
|
||||
import { onDestroy } from 'svelte'
|
||||
import activity, { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity'
|
||||
import { ActivityScrolledView } from '@hcengineering/activity-resources'
|
||||
|
||||
import chunter from '../plugin'
|
||||
import { getDay, isMessageHighlighted, messageIdForScroll, scrollAndHighLight, shouldScrollToMessage } from '../utils'
|
||||
import ChannelSeparator from './ChannelSeparator.svelte'
|
||||
import JumpToDateSelector from './JumpToDateSelector.svelte'
|
||||
import MessageComponent from './Message.svelte'
|
||||
|
||||
export let space: Ref<Space> | undefined
|
||||
export let pinnedIds: Ref<ChunterMessage>[]
|
||||
export let savedMessagesIds: Ref<ChunterMessage>[]
|
||||
export let savedAttachmentsIds: Ref<Attachment>[]
|
||||
export let isScrollForced = false
|
||||
export let content: HTMLElement | undefined = undefined
|
||||
export let notifyContext: DocNotifyContext
|
||||
export let object: Doc
|
||||
export let filterId: Ref<ActivityMessagesFilter> = activity.ids.AllFilter
|
||||
|
||||
let autoscroll: boolean = false
|
||||
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
||||
|
||||
const unsubscribe = locationStore.subscribe((newLocation) => {
|
||||
const messageId = newLocation.fragment
|
||||
|
||||
if (!messageId) {
|
||||
messageIdForScroll.set('')
|
||||
|
||||
return
|
||||
}
|
||||
if (messageId === $messageIdForScroll) {
|
||||
return
|
||||
}
|
||||
messageIdForScroll.set(messageId)
|
||||
shouldScrollToMessage.set(true)
|
||||
scrollAndHighLight()
|
||||
selectedMessageId = newLocation.fragment as Ref<ActivityMessage>
|
||||
})
|
||||
|
||||
onDestroy(unsubscribe)
|
||||
|
||||
beforeUpdate(() => {
|
||||
autoscroll = content !== undefined && content.offsetHeight + content.scrollTop > content.scrollHeight - 20
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
if ($shouldScrollToMessage && !$isMessageHighlighted) {
|
||||
scrollAndHighLight()
|
||||
|
||||
return
|
||||
}
|
||||
if (content && (autoscroll || isScrollForced)) {
|
||||
content.scrollTo(0, content.scrollHeight)
|
||||
isScrollForced = false
|
||||
}
|
||||
})
|
||||
|
||||
let messages: WithLookup<Message>[] = []
|
||||
const query = createQuery()
|
||||
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const docUpdates = notificationClient.docUpdatesStore
|
||||
|
||||
$: updateQuery(space)
|
||||
|
||||
function updateQuery (space: Ref<Space> | undefined) {
|
||||
if (space === undefined) {
|
||||
query.unsubscribe()
|
||||
messages = []
|
||||
return
|
||||
}
|
||||
query.query(
|
||||
chunter.class.Message,
|
||||
{
|
||||
space
|
||||
},
|
||||
(res) => {
|
||||
messages = res
|
||||
newMessagesPos = newMessagesStart(messages, $docUpdates)
|
||||
notificationClient.read(space)
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
_id: { attachments: attachment.class.Attachment, reactions: activity.class.Reaction },
|
||||
createBy: core.class.Account
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function newMessagesStart (messages: Message[], docUpdates: Map<Ref<Doc>, DocUpdates>): number {
|
||||
if (space === undefined) return -1
|
||||
const docUpdate = docUpdates.get(space)
|
||||
const lastView = docUpdate?.txes?.findLast((tx) => !tx.isNew)
|
||||
if (!docUpdate?.txes.some((tx) => tx.isNew)) return -1
|
||||
if (docUpdate === undefined || lastView === undefined) return -1
|
||||
for (let index = 0; index < messages.length; index++) {
|
||||
const message = messages[index]
|
||||
if ((message.createdOn ?? 0) >= lastView.modifiedOn) return index
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
$: markUnread(messages, $docUpdates)
|
||||
function markUnread (messages: Message[], docUpdates: Map<Ref<Doc>, DocUpdates>) {
|
||||
const newPos = newMessagesStart(messages, docUpdates)
|
||||
if (newPos !== -1) {
|
||||
newMessagesPos = newPos
|
||||
}
|
||||
}
|
||||
|
||||
let newMessagesPos: number = -1
|
||||
|
||||
function isOtherDay (time1: Timestamp, time2: Timestamp) {
|
||||
return getDay(time1) !== getDay(time2)
|
||||
}
|
||||
|
||||
function handleJumpToDate (e: CustomEvent<any>) {
|
||||
const date = e.detail.date
|
||||
if (!date) {
|
||||
return
|
||||
}
|
||||
|
||||
const dateSelectors = content?.getElementsByClassName('dateSelector')
|
||||
if (!dateSelectors) return
|
||||
|
||||
let closestDate: Timestamp | undefined = parseInt(dateSelectors[dateSelectors.length - 1].id)
|
||||
|
||||
for (const elem of Array.from(dateSelectors).reverse()) {
|
||||
const curDate = parseInt(elem.id)
|
||||
if (curDate < date) break
|
||||
else if (curDate - date < closestDate - date) {
|
||||
closestDate = curDate
|
||||
}
|
||||
}
|
||||
if (closestDate && closestDate < date) closestDate = undefined
|
||||
|
||||
if (closestDate) {
|
||||
scrollToDate(closestDate)
|
||||
}
|
||||
}
|
||||
|
||||
const pinnedHeight = 30
|
||||
const headerHeight = 50
|
||||
function scrollToDate (date: Timestamp) {
|
||||
let offset = date && document.getElementById(date.toString())?.offsetTop
|
||||
if (offset) {
|
||||
offset = offset - headerHeight - dateSelectorHeight / 2
|
||||
if (pinnedIds.length > 0) offset = offset - pinnedHeight
|
||||
content?.scrollTo({ left: 0, top: offset })
|
||||
}
|
||||
}
|
||||
|
||||
let showFixed: boolean | undefined
|
||||
let selectedDate: Timestamp | undefined = undefined
|
||||
function handleScroll () {
|
||||
const upperVisible = getFirstVisible()
|
||||
if (upperVisible) {
|
||||
selectedDate = parseInt(upperVisible.id)
|
||||
}
|
||||
}
|
||||
|
||||
const dateSelectorHeight = 30
|
||||
function getFirstVisible (): Element | undefined {
|
||||
if (!content) return
|
||||
|
||||
const clientRect = content.getBoundingClientRect()
|
||||
const dateSelectors = content.getElementsByClassName('dateSelector')
|
||||
const firstVisible = Array.from(dateSelectors)
|
||||
.reverse()
|
||||
.find((child) => {
|
||||
if (child?.nodeType === Node.ELEMENT_NODE) {
|
||||
const rect = child?.getBoundingClientRect()
|
||||
if (rect.top - dateSelectorHeight / 2 <= clientRect.top + dateSelectorHeight) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
if (firstVisible) {
|
||||
showFixed = clientRect.top - firstVisible.getBoundingClientRect().top > -dateSelectorHeight / 2
|
||||
}
|
||||
return firstVisible
|
||||
}
|
||||
$: isDocChannel = ![chunter.class.DirectMessage, chunter.class.Channel].includes(notifyContext.attachedToClass)
|
||||
$: messagesClass = isDocChannel ? activity.class.ActivityMessage : chunter.class.ChatMessage
|
||||
$: collection = isDocChannel ? 'comments' : 'messages'
|
||||
</script>
|
||||
|
||||
<div class="flex-col vScroll" bind:this={content} on:scroll={handleScroll}>
|
||||
<div class="grower" />
|
||||
{#if showFixed}
|
||||
<div class="ml-2 pr-2 fixed">
|
||||
<JumpToDateSelector {selectedDate} fixed on:jumpToDate={handleJumpToDate} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if messages}
|
||||
{#each messages as message, i (message._id)}
|
||||
{#if newMessagesPos === i}
|
||||
<ChannelSeparator title={chunter.string.New} line reverse isNew />
|
||||
{/if}
|
||||
{#if i === 0 || isOtherDay(message.createdOn ?? 0, messages[i - 1].createdOn ?? 0)}
|
||||
<JumpToDateSelector selectedDate={message.createdOn} on:jumpToDate={handleJumpToDate} />
|
||||
{/if}
|
||||
<MessageComponent
|
||||
isHighlighted={$messageIdForScroll === message._id && $isMessageHighlighted}
|
||||
{message}
|
||||
on:openThread
|
||||
isPinned={pinnedIds.includes(message._id)}
|
||||
isSaved={savedMessagesIds.includes(message._id)}
|
||||
{savedAttachmentsIds}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.grower {
|
||||
flex-grow: 10;
|
||||
flex-shrink: 5;
|
||||
}
|
||||
.fixed {
|
||||
position: absolute;
|
||||
align-self: center;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
<ActivityScrolledView
|
||||
_class={messagesClass}
|
||||
{object}
|
||||
skipLabels={!isDocChannel}
|
||||
filter={filterId}
|
||||
startFromBottom
|
||||
{selectedMessageId}
|
||||
{collection}
|
||||
lastViewedTimestamp={notifyContext.lastViewedTimestamp}
|
||||
/>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
// 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
|
||||
@ -13,38 +13,38 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Channel } from '@hcengineering/chunter'
|
||||
import type { Ref } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { openDoc } from '@hcengineering/view-resources'
|
||||
import chunter from '../plugin'
|
||||
import { classIcon } from '../utils'
|
||||
import Header from './Header.svelte'
|
||||
import Lock from './icons/Lock.svelte'
|
||||
|
||||
export let spaceId: Ref<Channel> | undefined
|
||||
import Header from './Header.svelte'
|
||||
import chunter from '../plugin'
|
||||
import { getChannelIcon } from '../utils'
|
||||
|
||||
export let object: Doc
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
let channel: Channel | undefined
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: query.query(chunter.class.Channel, { _id: spaceId }, (result) => {
|
||||
channel = result[0]
|
||||
})
|
||||
$: topic = hierarchy.isDerived(object._class, chunter.class.Channel) ? (object as Channel).topic : undefined
|
||||
|
||||
async function onSpaceEdit (): Promise<void> {
|
||||
if (channel === undefined) return
|
||||
openDoc(client.getHierarchy(), channel)
|
||||
async function getTitle (object: Doc) {
|
||||
if (object._class === chunter.class.DirectMessage) {
|
||||
return await getDocTitle(client, object._id, object._class, object)
|
||||
}
|
||||
return await getDocLinkTitle(client, object._id, object._class, object)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ac-header divide full caption-height">
|
||||
{#if channel}
|
||||
{#await getTitle(object) then title}
|
||||
<Header
|
||||
icon={channel.private ? Lock : classIcon(client, channel._class)}
|
||||
label={channel.name}
|
||||
description={channel.topic}
|
||||
on:click={onSpaceEdit}
|
||||
icon={getChannelIcon(object)}
|
||||
iconProps={{ value: object }}
|
||||
label={title}
|
||||
intlLabel={title ? undefined : chunter.string.Channel}
|
||||
description={topic}
|
||||
/>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
@ -15,22 +15,20 @@
|
||||
<script lang="ts">
|
||||
import { SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import chunter, { ChunterMessage, DirectMessage } from '@hcengineering/chunter'
|
||||
import attachment from '@hcengineering/attachment'
|
||||
import chunter, { ChatMessage, DirectMessage } from '@hcengineering/chunter'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { ActivityMessagePresenter } from '@hcengineering/activity-resources'
|
||||
|
||||
import chunterResources from '../plugin'
|
||||
import MessagePreview from './MessagePreview.svelte'
|
||||
|
||||
export let object: DirectMessage
|
||||
export let newTxes: number
|
||||
|
||||
const NUM_OF_RECENT_MESSAGES = 5 as const
|
||||
|
||||
let messages: ChunterMessage[] = []
|
||||
let messages: ChatMessage[] = []
|
||||
const messagesQuery = createQuery()
|
||||
$: messagesQuery.query(
|
||||
chunter.class.ChunterMessage,
|
||||
chunter.class.ChatMessage,
|
||||
{ attachedTo: object._id },
|
||||
(res) => {
|
||||
if (res !== undefined) {
|
||||
@ -38,12 +36,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
limit: newTxes + NUM_OF_RECENT_MESSAGES,
|
||||
limit: NUM_OF_RECENT_MESSAGES,
|
||||
sort: {
|
||||
createdOn: SortingOrder.Descending
|
||||
},
|
||||
lookup: {
|
||||
_id: { attachments: attachment.class.Attachment }
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -52,7 +47,7 @@
|
||||
<div class="flex-col flex-gap-3 preview-container">
|
||||
{#if messages.length}
|
||||
{#each messages as message}
|
||||
<MessagePreview value={message} />
|
||||
<ActivityMessagePresenter value={message} skipLabel />
|
||||
{/each}
|
||||
{:else}
|
||||
<Label label={chunterResources.string.NoMessages} />
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
// 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
|
||||
@ -13,97 +13,34 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import { ChunterMessage, ChunterSpace, Message } from '@hcengineering/chunter'
|
||||
import { AttachedDoc, Ref, Space, generateId, getCurrentAccount } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Ref, Doc } from '@hcengineering/core'
|
||||
import { getLocation, navigate } from '@hcengineering/ui'
|
||||
import chunter from '../plugin'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
import activity, { 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 space: Ref<Space>
|
||||
let chunterSpace: ChunterSpace
|
||||
let isScrollForced = false
|
||||
export let notifyContext: DocNotifyContext
|
||||
export let object: Doc
|
||||
export let filterId: Ref<ActivityMessagesFilter> = activity.ids.AllFilter
|
||||
|
||||
const client = getClient()
|
||||
const _class = chunter.class.Message
|
||||
let _id = generateId<AttachedDoc>()
|
||||
|
||||
async function onMessage (event: CustomEvent) {
|
||||
const { message, attachments } = event.detail
|
||||
const me = getCurrentAccount()._id
|
||||
await client.addCollection(
|
||||
_class,
|
||||
space,
|
||||
space,
|
||||
chunterSpace?._class ?? chunter.class.ChunterSpace,
|
||||
'messages',
|
||||
{
|
||||
content: message,
|
||||
createBy: me,
|
||||
attachments
|
||||
},
|
||||
_id
|
||||
)
|
||||
|
||||
_id = generateId()
|
||||
isScrollForced = true
|
||||
loading = false
|
||||
}
|
||||
|
||||
function openThread (_id: Ref<Message>) {
|
||||
function openThread (_id: Ref<ChatMessage>) {
|
||||
const loc = getLocation()
|
||||
loc.path[4] = _id
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
const pinnedQuery = createQuery()
|
||||
let pinnedIds: Ref<ChunterMessage>[] = []
|
||||
pinnedQuery.query(
|
||||
chunter.class.ChunterSpace,
|
||||
{ _id: space },
|
||||
(res) => {
|
||||
pinnedIds = res[0]?.pinned ?? []
|
||||
chunterSpace = res[0]
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
|
||||
const savedMessagesQuery = createQuery()
|
||||
let savedMessagesIds: Ref<ChunterMessage>[] = []
|
||||
savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
|
||||
savedMessagesIds = res.map((r) => r.attachedTo)
|
||||
})
|
||||
|
||||
const savedAttachmentsQuery = createQuery()
|
||||
let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
|
||||
savedAttachmentsIds = res.map((r) => r.attachedTo)
|
||||
})
|
||||
let loading = false
|
||||
let content: HTMLElement
|
||||
</script>
|
||||
|
||||
<PinnedMessages {space} {pinnedIds} />
|
||||
<ChannelHeader {object} />
|
||||
<PinnedMessages {notifyContext} />
|
||||
<Channel
|
||||
bind:isScrollForced
|
||||
bind:content
|
||||
{space}
|
||||
{notifyContext}
|
||||
{object}
|
||||
{filterId}
|
||||
on:openThread={(e) => {
|
||||
openThread(e.detail)
|
||||
}}
|
||||
{pinnedIds}
|
||||
{savedMessagesIds}
|
||||
{savedAttachmentsIds}
|
||||
/>
|
||||
<div class="reference">
|
||||
<AttachmentRefInput bind:loading {space} {_class} objectId={_id} boundary={content} on:message={onMessage} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.reference {
|
||||
margin: 1.25rem 1rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -13,14 +13,32 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, Space } from '@hcengineering/core'
|
||||
import ChannelView from './ChannelView.svelte'
|
||||
import SpaceHeader from './SpaceHeader.svelte'
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import notification, { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
|
||||
export let _id: Ref<Space>
|
||||
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>
|
||||
|
||||
<div class="antiComponent">
|
||||
<SpaceHeader spaceId={_id} withSearch={false} />
|
||||
<ChannelView space={_id} />
|
||||
</div>
|
||||
{#if notifyContext && object}
|
||||
<div class="antiComponent">
|
||||
<ChannelPresenter {notifyContext} {object} />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
// 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 { DirectMessage } from '@hcengineering/chunter'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { Avatar } from '@hcengineering/contact-resources'
|
||||
|
||||
import { getDmPersons } from '../utils'
|
||||
|
||||
export let value: DirectMessage
|
||||
|
||||
const client = getClient()
|
||||
let persons: Person[] = []
|
||||
|
||||
$: getDmPersons(client, value).then((res) => {
|
||||
persons = res
|
||||
})
|
||||
</script>
|
||||
|
||||
{#each persons as person}
|
||||
{#if person}
|
||||
<div class="icon">
|
||||
<Avatar size="x-small" avatar={person.avatar} name={person.name} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
@ -15,28 +15,22 @@
|
||||
<script lang="ts">
|
||||
import { chunterId, DirectMessage } from '@hcengineering/chunter'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Icon } from '@hcengineering/ui'
|
||||
import { NavLink } from '@hcengineering/view-resources'
|
||||
|
||||
import { getDmName } from '../utils'
|
||||
import DmIconPresenter from './DmIconPresenter.svelte'
|
||||
|
||||
export let value: DirectMessage
|
||||
export let disabled = false
|
||||
|
||||
const client = getClient()
|
||||
|
||||
$: icon = client.getHierarchy().getClass(value._class).icon
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#await getDmName(client, value) then name}
|
||||
<NavLink app={chunterId} space={value._id} {disabled}>
|
||||
<div class="flex-presenter">
|
||||
<div class="icon">
|
||||
{#if icon}
|
||||
<Icon {icon} size={'small'} />
|
||||
{/if}
|
||||
</div>
|
||||
<DmIconPresenter {value} />
|
||||
<span class="label">{name}</span>
|
||||
</div>
|
||||
</NavLink>
|
||||
|
@ -19,6 +19,7 @@
|
||||
import { navigateToSpecial } from '../utils'
|
||||
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let iconProps: Record<string, any> | undefined = undefined
|
||||
export let label: string | undefined = undefined
|
||||
export let intlLabel: IntlString | undefined = undefined
|
||||
export let description: string | undefined = undefined
|
||||
@ -31,7 +32,7 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="ac-header__wrap-title" on:click>
|
||||
{#if icon}<div class="ac-header__icon"><Icon {icon} size={'small'} /></div>{/if}
|
||||
{#if icon}<div class="ac-header__icon"><Icon {icon} size={'small'} {iconProps} /></div>{/if}
|
||||
{#if label}
|
||||
<span class="ac-header__title">{label}</span>
|
||||
{:else if intlLabel}
|
||||
|
@ -1,347 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachmentList, AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import type { ChunterMessage, ChunterMessageExtension, Message } from '@hcengineering/chunter'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import { Avatar, personByIdStore, EmployeePresenter } from '@hcengineering/contact-resources'
|
||||
import { getCurrentAccount, getDisplayTime, Mixin, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import ui, { ActionIcon, Button, EmojiPopup, IconMoreV, Label, showPopup, tooltip } from '@hcengineering/ui'
|
||||
import { Action } from '@hcengineering/view'
|
||||
import { LinkPresenter, Menu, ObjectPresenter } from '@hcengineering/view-resources'
|
||||
import notification, { Collaborators } from '@hcengineering/notification'
|
||||
import { Reactions, updateDocReactions } from '@hcengineering/activity-resources'
|
||||
import { Reaction } from '@hcengineering/activity'
|
||||
|
||||
import Bookmark from './icons/Bookmark.svelte'
|
||||
import Emoji from './icons/Emoji.svelte'
|
||||
import Thread from './icons/Thread.svelte'
|
||||
import Replies from './Replies.svelte'
|
||||
|
||||
import { AddMessageToSaved, DeleteMessageFromSaved, UnpinMessage } from '../index'
|
||||
import chunter from '../plugin'
|
||||
import { getLinks } from '../utils'
|
||||
|
||||
export let message: WithLookup<ChunterMessage>
|
||||
export let savedAttachmentsIds: Ref<Attachment>[]
|
||||
export let thread: boolean = false
|
||||
export let isPinned: boolean = false
|
||||
export let isSaved: boolean = false
|
||||
export let isHighlighted = false
|
||||
export let readOnly = false
|
||||
|
||||
let refInput: AttachmentRefInput
|
||||
|
||||
$: empRef = (message.$lookup?.createBy as PersonAccount)?.person
|
||||
$: employee = empRef !== undefined ? $personByIdStore.get(empRef) : undefined
|
||||
$: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const dispatch = createEventDispatcher()
|
||||
const me = getCurrentAccount()._id
|
||||
|
||||
$: reactions = message.$lookup?.reactions as Reaction[] | undefined
|
||||
|
||||
$: subscribed = (
|
||||
hierarchy.as(message, notification.mixin.Collaborators) as any as Collaborators
|
||||
).collaborators?.includes(me)
|
||||
$: subscribeAction = subscribed
|
||||
? ({
|
||||
label: chunter.string.TurnOffReplies,
|
||||
action: chunter.actionImpl.UnsubscribeMessage
|
||||
} as unknown as Action)
|
||||
: ({
|
||||
label: chunter.string.GetNewReplies,
|
||||
action: chunter.actionImpl.SubscribeMessage
|
||||
} as unknown as Action)
|
||||
|
||||
$: pinActions = isPinned
|
||||
? ({
|
||||
label: chunter.string.UnpinMessage,
|
||||
action: chunter.actionImpl.UnpinMessage
|
||||
} as unknown as Action)
|
||||
: ({
|
||||
label: chunter.string.PinMessage,
|
||||
action: chunter.actionImpl.PinMessage
|
||||
} as unknown as Action)
|
||||
|
||||
$: isEditing = false
|
||||
|
||||
let extensions: Ref<Mixin<ChunterMessageExtension>>[] = []
|
||||
$: if (message) {
|
||||
extensions = []
|
||||
for (const extension of hierarchy.getDescendants(chunter.mixin.ChunterMessageExtension)) {
|
||||
if (hierarchy.hasMixin(message, extension)) {
|
||||
extensions.push(extension as Ref<Mixin<ChunterMessageExtension>>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const editAction = {
|
||||
label: chunter.string.EditMessage,
|
||||
action: () => (isEditing = true)
|
||||
}
|
||||
|
||||
const deleteAction = {
|
||||
label: chunter.string.DeleteMessage,
|
||||
action: async () => {
|
||||
;(await client.findAll(chunter.class.ThreadMessage, { attachedTo: message._id as Ref<Message> })).forEach((c) => {
|
||||
UnpinMessage(c)
|
||||
DeleteMessageFromSaved(c)
|
||||
})
|
||||
UnpinMessage(message)
|
||||
await client.removeDoc(message._class, message.space, message._id)
|
||||
}
|
||||
}
|
||||
|
||||
let menuShowed = false
|
||||
|
||||
const showMenu = async (ev: Event): Promise<void> => {
|
||||
const actions = [pinActions]
|
||||
if (message._class === chunter.class.Message) {
|
||||
actions.push(subscribeAction)
|
||||
}
|
||||
|
||||
menuShowed = true
|
||||
showPopup(
|
||||
Menu,
|
||||
{
|
||||
object: message,
|
||||
baseMenuClass: message._class,
|
||||
actions: [
|
||||
...actions.map((a) => ({
|
||||
label: a.label,
|
||||
icon: a.icon,
|
||||
action: async (ctx: any, evt: MouseEvent) => {
|
||||
const impl = await getResource(a.action)
|
||||
await impl(message, evt)
|
||||
}
|
||||
})),
|
||||
...(getCurrentAccount()._id === message.createBy ? [editAction, deleteAction] : [])
|
||||
]
|
||||
},
|
||||
ev.target as HTMLElement,
|
||||
() => {
|
||||
menuShowed = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function onMessageEdit (event: CustomEvent) {
|
||||
const { message: newContent, attachments: newAttachments } = event.detail
|
||||
|
||||
await client.update(message, {
|
||||
content: newContent,
|
||||
attachments: newAttachments
|
||||
})
|
||||
isEditing = false
|
||||
loading = false
|
||||
}
|
||||
|
||||
function openThread () {
|
||||
dispatch('openThread', message._id)
|
||||
}
|
||||
|
||||
function addToSaved () {
|
||||
if (isSaved) DeleteMessageFromSaved(message)
|
||||
else AddMessageToSaved(message)
|
||||
}
|
||||
|
||||
function updateReactions (emoji?: string) {
|
||||
updateDocReactions(client, reactions || [], message, emoji)
|
||||
}
|
||||
|
||||
function openEmojiPalette (ev: Event) {
|
||||
showPopup(EmojiPopup, {}, ev.target as HTMLElement, updateReactions, () => {})
|
||||
}
|
||||
|
||||
$: parentMessage = message as Message
|
||||
$: hasReplies = (parentMessage?.replies?.length ?? 0) > 0
|
||||
|
||||
$: links = getLinks(message.content)
|
||||
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<div class="container clear-mins" class:highlighted={isHighlighted} id={message._id}>
|
||||
<div class="min-w-6">
|
||||
<Avatar size="x-small" avatar={employee?.avatar} name={employee?.name} />
|
||||
</div>
|
||||
<div class="message ml-2 w-full clear-mins">
|
||||
<div class="header clear-mins">
|
||||
{#if employee}
|
||||
<EmployeePresenter value={employee} shouldShowAvatar={false} />
|
||||
{/if}
|
||||
<span class="text-sm">{getDisplayTime(message.createdOn ?? 0)}</span>
|
||||
{#if message.editedOn}
|
||||
<span use:tooltip={{ label: ui.string.TimeTooltip, props: { value: getDisplayTime(message.editedOn) } }}>
|
||||
<Label label={chunter.string.Edited} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isEditing}
|
||||
<AttachmentRefInput
|
||||
bind:this={refInput}
|
||||
space={message.space}
|
||||
_class={message._class}
|
||||
objectId={message._id}
|
||||
content={message.content}
|
||||
showSend={false}
|
||||
on:message={onMessageEdit}
|
||||
bind:loading
|
||||
/>
|
||||
<div class="flex-row-reverse gap-2 reverse">
|
||||
<Button label={chunter.string.EditCancel} on:click={() => (isEditing = false)} />
|
||||
<Button label={chunter.string.EditUpdate} on:click={() => refInput.submit()} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text"><MessageViewer message={message.content} /></div>
|
||||
{#if message.attachments}
|
||||
<div class="attachments">
|
||||
<AttachmentList {attachments} {savedAttachmentsIds} />
|
||||
</div>
|
||||
{/if}
|
||||
{#each links as link}
|
||||
<LinkPresenter {link} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if reactions?.length || (!thread && hasReplies)}
|
||||
<div class="footer flex-col">
|
||||
{#if reactions?.length}
|
||||
<Reactions
|
||||
{reactions}
|
||||
on:click={(ev) => {
|
||||
updateReactions(ev.detail)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if !thread && hasReplies}
|
||||
<Replies message={parentMessage} on:click={openThread} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="buttons clear-mins flex flex-gap-1 items-center" class:menuShowed>
|
||||
{#each extensions as mixinClass}
|
||||
<ObjectPresenter _class={mixinClass} value={hierarchy.as(message, mixinClass)} exact />
|
||||
{/each}
|
||||
{#if !readOnly}
|
||||
<ActionIcon icon={Emoji} size={'medium'} action={openEmojiPalette} />
|
||||
<div class="book">
|
||||
<ActionIcon
|
||||
icon={Bookmark}
|
||||
size={'medium'}
|
||||
action={addToSaved}
|
||||
label={isSaved ? chunter.string.RemoveFromSaved : chunter.string.AddToSaved}
|
||||
/>
|
||||
</div>
|
||||
{#if !thread}
|
||||
<ActionIcon icon={Thread} size={'medium'} action={openThread} />
|
||||
{/if}
|
||||
|
||||
<ActionIcon
|
||||
icon={IconMoreV}
|
||||
size={'medium'}
|
||||
action={(e) => {
|
||||
showMenu(e)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@keyframes highlight {
|
||||
50% {
|
||||
background-color: var(--theme-warning-color);
|
||||
}
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&.highlighted {
|
||||
animation: highlight 2000ms ease-in-out;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
line-height: 150%;
|
||||
color: var(--theme-caption-color);
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
span {
|
||||
margin-left: 0.25rem;
|
||||
font-weight: 400;
|
||||
|
||||
line-height: 1.25rem;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
line-height: 150%;
|
||||
user-select: contain;
|
||||
}
|
||||
.attachments {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.footer {
|
||||
align-items: flex-start;
|
||||
margin-top: 0.5rem;
|
||||
user-select: none;
|
||||
|
||||
div + div {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
user-select: none;
|
||||
color: var(--theme-halfcontent-color);
|
||||
|
||||
&.menuShowed {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .buttons {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--highlight-hover);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,62 +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, { Message } from '@hcengineering/chunter'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { MessageViewer, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Icon, Label } from '@hcengineering/ui'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import { getObjectPresenter } from '@hcengineering/view-resources'
|
||||
|
||||
import chunterResources from '../plugin'
|
||||
|
||||
export let value: Message
|
||||
export let inline: boolean = false
|
||||
export let embedded = false
|
||||
export let disabled = false
|
||||
|
||||
const client = getClient()
|
||||
const isThreadMessage = client.getHierarchy().isDerived(value._class, chunter.class.ThreadMessage)
|
||||
|
||||
let presenter: AttributeModel | undefined
|
||||
getObjectPresenter(client, value.attachedToClass, { key: '' }).then((p) => {
|
||||
presenter = p
|
||||
})
|
||||
|
||||
let doc: Doc | undefined = undefined
|
||||
const docQuery = createQuery()
|
||||
$: docQuery.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
|
||||
;[doc] = res
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if inline || embedded}
|
||||
{#if presenter && doc}
|
||||
<div class="flex-presenter">
|
||||
{#if isThreadMessage}
|
||||
<div class="icon">
|
||||
<Icon icon={chunter.icon.Thread} size="small" />
|
||||
</div>
|
||||
<span class="labels-row" style:text-transform="lowercase">
|
||||
<Label label={chunterResources.string.On} />
|
||||
</span>
|
||||
|
||||
{/if}
|
||||
<svelte:component this={presenter.presenter} value={doc} {inline} {disabled} shouldShowAvatarr={false} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div><MessageViewer message={value.content} /></div>
|
||||
{/if}
|
@ -1,119 +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 { ChunterMessage } from '@hcengineering/chunter'
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
import ui, { Label, tooltip } from '@hcengineering/ui'
|
||||
import { LinkPresenter } from '@hcengineering/view-resources'
|
||||
import { AttachmentList } from '@hcengineering/attachment-resources'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { Ref, WithLookup, getCurrentAccount, getDisplayTime } from '@hcengineering/core'
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { EmployeePresenter, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
|
||||
import chunter from '../plugin'
|
||||
import { getLinks } from '../utils'
|
||||
|
||||
export let value: WithLookup<ChunterMessage>
|
||||
|
||||
$: attachments = (value.$lookup?.attachments ?? []) as Attachment[]
|
||||
|
||||
$: links = getLinks(value.content)
|
||||
|
||||
const me = getCurrentAccount()._id as Ref<PersonAccount>
|
||||
|
||||
let account: PersonAccount | undefined
|
||||
|
||||
$: account = $personAccountByIdStore.get(value.createdBy as Ref<PersonAccount>)
|
||||
$: employee = account && $personByIdStore.get(account.person)
|
||||
</script>
|
||||
|
||||
<div class="container clear-mins" class:highlighted={false} id={value._id}>
|
||||
<div class="message clear-mins">
|
||||
<div class="flex-row-center header clear-mins">
|
||||
{#if employee && account}
|
||||
{#if account._id !== me}
|
||||
<EmployeePresenter value={employee} shouldShowAvatar={true} disabled />
|
||||
{:else}
|
||||
<Label label={chunter.string.You} />
|
||||
{/if}
|
||||
{/if}
|
||||
<span>{getDisplayTime(value.createdOn ?? 0)}</span>
|
||||
{#if value.editedOn}
|
||||
<span use:tooltip={{ label: ui.string.TimeTooltip, props: { value: getDisplayTime(value.editedOn) } }}>
|
||||
<Label label={getEmbeddedLabel('Edited')} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text"><MessageViewer message={value.content} /></div>
|
||||
{#if value.attachments}
|
||||
<div class="attachments">
|
||||
<AttachmentList {attachments} />
|
||||
</div>
|
||||
{/if}
|
||||
{#each links as link}
|
||||
<LinkPresenter {link} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@keyframes highlight {
|
||||
50% {
|
||||
background-color: var(--theme-warning-color);
|
||||
}
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem 0.15rem;
|
||||
|
||||
&.highlighted {
|
||||
animation: highlight 2000ms ease-in-out;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-left: 1rem;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
line-height: 150%;
|
||||
color: var(--theme-caption-color);
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
span {
|
||||
margin-left: 0.5rem;
|
||||
font-weight: 400;
|
||||
|
||||
line-height: 1.125rem;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
line-height: 150%;
|
||||
user-select: contain;
|
||||
}
|
||||
.attachments {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,117 +0,0 @@
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import chunter, { ChunterMessage } from '@hcengineering/chunter'
|
||||
import core, { DocumentQuery, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Label, Scroller, SearchEdit } from '@hcengineering/ui'
|
||||
import { FilterBar } from '@hcengineering/view-resources'
|
||||
import plugin from '../plugin'
|
||||
import { openMessageFromSpecial } from '../utils'
|
||||
import MessageComponent from './Message.svelte'
|
||||
|
||||
export let withHeader: boolean = true
|
||||
export let filterClass = chunter.class.ChunterMessage
|
||||
export let search: string = ''
|
||||
|
||||
let searchQuery: DocumentQuery<ChunterMessage> = { $search: search }
|
||||
|
||||
function updateSearchQuery (search: string): void {
|
||||
searchQuery = { $search: search }
|
||||
}
|
||||
|
||||
$: updateSearchQuery(search)
|
||||
|
||||
const client = getClient()
|
||||
let messages: ChunterMessage[] = []
|
||||
|
||||
let resultQuery: DocumentQuery<ChunterMessage> = { ...searchQuery }
|
||||
|
||||
async function updateMessages (resultQuery: DocumentQuery<ChunterMessage>) {
|
||||
messages = await client.findAll(
|
||||
filterClass,
|
||||
{
|
||||
...resultQuery
|
||||
},
|
||||
{
|
||||
sort: { createdOn: SortingOrder.Descending },
|
||||
limit: 100,
|
||||
lookup: {
|
||||
_id: { attachments: attachment.class.Attachment },
|
||||
createBy: core.class.Account
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$: updateMessages(resultQuery)
|
||||
|
||||
const pinnedQuery = createQuery()
|
||||
const pinnedIds: Ref<ChunterMessage>[] = []
|
||||
|
||||
pinnedQuery.query(
|
||||
chunter.class.Channel,
|
||||
{},
|
||||
(res) => {
|
||||
res.forEach((ch) => {
|
||||
if (ch.pinned) {
|
||||
pinnedIds.push(...ch.pinned)
|
||||
}
|
||||
})
|
||||
},
|
||||
{}
|
||||
)
|
||||
let savedMessagesIds: Ref<ChunterMessage>[] = []
|
||||
let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||
|
||||
const savedMessagesQuery = createQuery()
|
||||
const savedAttachmentsQuery = createQuery()
|
||||
|
||||
savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
|
||||
savedMessagesIds = res.map((r) => r.attachedTo)
|
||||
})
|
||||
|
||||
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
|
||||
savedAttachmentsIds = res.map((r) => r.attachedTo)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if withHeader}
|
||||
<div class="ac-header full divide">
|
||||
<div class="ac-header__wrap-title">
|
||||
<span class="ac-header__title"><Label label={plugin.string.MessagesBrowser} /></span>
|
||||
</div>
|
||||
<SearchEdit
|
||||
value={search}
|
||||
on:change={() => {
|
||||
updateSearchQuery(search)
|
||||
updateMessages(resultQuery)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<FilterBar _class={filterClass} space={undefined} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
|
||||
{#if messages.length > 0}
|
||||
<Scroller>
|
||||
{#each messages as message}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
on:click={() => {
|
||||
openMessageFromSpecial(message)
|
||||
}}
|
||||
>
|
||||
<MessageComponent
|
||||
{message}
|
||||
on:openThread
|
||||
isPinned={pinnedIds.includes(message._id)}
|
||||
isSaved={savedMessagesIds.includes(message._id)}
|
||||
{savedAttachmentsIds}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</Scroller>
|
||||
{:else}
|
||||
<div class="flex-center h-full text-lg">
|
||||
<Label label={plugin.string.NoResults} />
|
||||
</div>
|
||||
{/if}
|
@ -1,29 +1,55 @@
|
||||
<!--
|
||||
// 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 { ChunterMessage } from '@hcengineering/chunter'
|
||||
import { Ref, Space } from '@hcengineering/core'
|
||||
import { eventToHTMLElement, Label, showPopup } from '@hcengineering/ui'
|
||||
import PinnedMessagesPopup from './PinnedMessagesPopup.svelte'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let space: Ref<Space>
|
||||
export let pinnedIds: Ref<ChunterMessage>[]
|
||||
export let notifyContext: DocNotifyContext
|
||||
|
||||
function showMessages (ev: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) {
|
||||
showPopup(PinnedMessagesPopup, { space }, eventToHTMLElement(ev))
|
||||
const pinnedQuery = createQuery()
|
||||
|
||||
let pinnedMessagesCount = 0
|
||||
|
||||
$: pinnedQuery.query(
|
||||
activity.class.ActivityMessage,
|
||||
{ attachedTo: notifyContext.attachedTo, isPinned: true },
|
||||
(res: ActivityMessage[]) => {
|
||||
pinnedMessagesCount = res.length
|
||||
}
|
||||
)
|
||||
function openMessagesPopup (ev: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) {
|
||||
showPopup(
|
||||
PinnedMessagesPopup,
|
||||
{ attachedTo: notifyContext.attachedTo, attachedToClass: notifyContext.attachedToClass },
|
||||
eventToHTMLElement(ev)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if pinnedIds.length > 0}
|
||||
{#if pinnedMessagesCount > 0}
|
||||
<div class="bottom-divider over-underline pt-2 pb-2 container">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
on:click={(ev) => {
|
||||
showMessages(ev)
|
||||
}}
|
||||
>
|
||||
<div on:click={openMessagesPopup}>
|
||||
<Label label={chunter.string.Pinned} />
|
||||
{pinnedIds.length}
|
||||
{pinnedMessagesCount}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -1,76 +1,37 @@
|
||||
<!--
|
||||
// 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, { ChunterMessage } from '@hcengineering/chunter'
|
||||
import { Person, PersonAccount, getName } from '@hcengineering/contact'
|
||||
import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { getDisplayTime, IdMap, Ref, Space } from '@hcengineering/core'
|
||||
import { MessageViewer, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { IconClose } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { UnpinMessage } from '../index'
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import { ActivityMessagePresenter } from '@hcengineering/activity-resources'
|
||||
|
||||
export let space: Ref<Space>
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const pinnedQuery = createQuery()
|
||||
let pinnedIds: Ref<ChunterMessage>[] = []
|
||||
pinnedQuery.query(
|
||||
chunter.class.ChunterSpace,
|
||||
{ _id: space },
|
||||
(res) => {
|
||||
pinnedIds = res[0]?.pinned ?? []
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
export let attachedTo: Ref<Doc>
|
||||
|
||||
const messagesQuery = createQuery()
|
||||
let pinnedMessages: ChunterMessage[] = []
|
||||
|
||||
$: pinnedIds &&
|
||||
messagesQuery.query(chunter.class.ChunterMessage, { _id: { $in: pinnedIds } }, (res) => {
|
||||
pinnedMessages = res
|
||||
})
|
||||
let pinnedMessages: DisplayActivityMessage[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function getEmployee (
|
||||
message: ChunterMessage,
|
||||
employeeAccounts: IdMap<PersonAccount>,
|
||||
employees: IdMap<Person>
|
||||
): Person | undefined {
|
||||
const acc = employeeAccounts.get(message.createBy as Ref<PersonAccount>)
|
||||
if (acc) {
|
||||
return employees.get(acc.person)
|
||||
}
|
||||
}
|
||||
$: messagesQuery.query(activity.class.ActivityMessage, { attachedTo, isPinned: true }, (res: ActivityMessage[]) => {
|
||||
pinnedMessages = res as DisplayActivityMessage[]
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="antiPopup vScroll popup">
|
||||
{#each pinnedMessages as message}
|
||||
{@const employee = getEmployee(message, $personAccountByIdStore, $personByIdStore)}
|
||||
<div class="message">
|
||||
<div class="header">
|
||||
<div class="avatar">
|
||||
<Avatar size={'medium'} avatar={employee?.avatar} name={employee?.name} />
|
||||
</div>
|
||||
<span class="name">
|
||||
{employee ? getName(client.getHierarchy(), employee) : ''}
|
||||
</span>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="cross"
|
||||
on:click={async () => {
|
||||
if (pinnedIds.length === 1) dispatch('close')
|
||||
UnpinMessage(message)
|
||||
}}
|
||||
>
|
||||
<IconClose size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<MessageViewer message={message.content} />
|
||||
<span class="time">{getDisplayTime(message.createdOn ?? 0)}</span>
|
||||
</div>
|
||||
<ActivityMessagePresenter value={message} withActions={false} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@ -80,34 +41,4 @@
|
||||
max-height: 20rem;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 0.75rem 1rem 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: inherit;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
margin-left: 1rem;
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.cross {
|
||||
opacity: 0.4;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.time {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,128 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022 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 { Message } from '@hcengineering/chunter'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { personByIdStore, Avatar } from '@hcengineering/contact-resources'
|
||||
import { Doc, IdMap, Ref } from '@hcengineering/core'
|
||||
import { Label, TimeSince } from '@hcengineering/ui'
|
||||
import { NotificationClientImpl } from '@hcengineering/notification-resources'
|
||||
import { DocUpdates } from '@hcengineering/notification'
|
||||
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let message: Message
|
||||
$: lastReply = message.lastReply ?? new Date().getTime()
|
||||
$: employees = new Set(message.replies)
|
||||
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const docUpdates = notificationClient.docUpdatesStore
|
||||
|
||||
const shown: number = 4
|
||||
let showReplies: Person[] = []
|
||||
|
||||
$: hasNew = checkNewReplies(message, $docUpdates)
|
||||
$: updateQuery(employees, $personByIdStore)
|
||||
|
||||
function checkNewReplies (message: Message, docUpdates: Map<Ref<Doc>, DocUpdates>): boolean {
|
||||
const docUpdate = docUpdates.get(message._id)
|
||||
if (docUpdate === undefined) return false
|
||||
return docUpdate.txes.filter((tx) => tx.isNew).length > 0
|
||||
}
|
||||
|
||||
function updateQuery (employees: Set<Ref<Person>>, map: IdMap<Person>) {
|
||||
showReplies = []
|
||||
for (const employee of employees) {
|
||||
const emp = map.get(employee)
|
||||
if (emp !== undefined) {
|
||||
showReplies.push(emp)
|
||||
}
|
||||
}
|
||||
showReplies = showReplies
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex-row-center container cursor-pointer" on:click>
|
||||
<div class="flex-row-center">
|
||||
{#each showReplies as reply}
|
||||
<div class="reply"><Avatar size={'x-small'} avatar={reply.avatar} name={reply.name} /></div>
|
||||
{/each}
|
||||
{#if employees.size > shown}
|
||||
<div class="reply"><span>+{employees.size - shown}</span></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="whitespace-nowrap ml-2 mr-2 over-underline">
|
||||
<Label label={chunter.string.RepliesCount} params={{ replies: message.replies?.length ?? 0 }} />
|
||||
</div>
|
||||
{#if hasNew}
|
||||
<div class="marker" />
|
||||
{/if}
|
||||
{#if (message.replies?.length ?? 0) > 1}
|
||||
<div class="mr-1">
|
||||
<Label label={chunter.string.LastReply} />
|
||||
</div>
|
||||
{/if}
|
||||
<TimeSince value={lastReply} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
|
||||
.reply {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--theme-bg-color);
|
||||
border-radius: 50%;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 0.5;
|
||||
color: var(--caption-color);
|
||||
background-color: var(--theme-bg-accent-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.reply + .reply {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--button-border-hover);
|
||||
background-color: var(--theme-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.marker {
|
||||
margin: 0 0.25rem 0 -0.25rem;
|
||||
width: 0.425rem;
|
||||
height: 0.425rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--highlight-red);
|
||||
}
|
||||
</style>
|
@ -1,175 +0,0 @@
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachmentPreview } from '@hcengineering/attachment-resources'
|
||||
import { ChunterMessage } from '@hcengineering/chunter'
|
||||
import { Person, PersonAccount, getName as getContactName } from '@hcengineering/contact'
|
||||
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import core, { getDisplayTime, IdMap, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Label, Scroller } from '@hcengineering/ui'
|
||||
import chunter from '../plugin'
|
||||
import { openMessageFromSpecial } from '../utils'
|
||||
import Message from './Message.svelte'
|
||||
import Bookmark from './icons/Bookmark.svelte'
|
||||
|
||||
const client = getClient()
|
||||
let savedMessagesIds: Ref<ChunterMessage>[] = []
|
||||
let savedMessages: WithLookup<ChunterMessage>[] = []
|
||||
let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||
let savedAttachments: WithLookup<Attachment>[] = []
|
||||
|
||||
const messagesQuery = createQuery()
|
||||
const attachmentsQuery = createQuery()
|
||||
const savedMessagesQuery = createQuery()
|
||||
const savedAttachmentsQuery = createQuery()
|
||||
|
||||
savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
|
||||
savedMessagesIds = res.map((r) => r.attachedTo)
|
||||
})
|
||||
|
||||
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
|
||||
savedAttachmentsIds = res.map((r) => r.attachedTo)
|
||||
})
|
||||
|
||||
$: savedMessagesIds &&
|
||||
messagesQuery.query(
|
||||
chunter.class.ChunterMessage,
|
||||
{ _id: { $in: savedMessagesIds } },
|
||||
(res) => {
|
||||
savedMessages = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
_id: { attachments: attachment.class.Attachment },
|
||||
createBy: core.class.Account
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$: savedAttachmentsIds &&
|
||||
attachmentsQuery.query(attachment.class.Attachment, { _id: { $in: savedAttachmentsIds } }, (res) => {
|
||||
savedAttachments = res
|
||||
})
|
||||
|
||||
const pinnedQuery = createQuery()
|
||||
const pinnedIds: Ref<ChunterMessage>[] = []
|
||||
|
||||
pinnedQuery.query(
|
||||
chunter.class.Channel,
|
||||
{},
|
||||
(res) => {
|
||||
res.forEach((ch) => {
|
||||
if (ch.pinned) {
|
||||
pinnedIds.push(...ch.pinned)
|
||||
}
|
||||
})
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
async function openAttachment (att: Attachment) {
|
||||
const messageId: Ref<ChunterMessage> = att.attachedTo as Ref<ChunterMessage>
|
||||
await client.findOne(chunter.class.ChunterMessage, { _id: messageId }).then((res) => {
|
||||
if (res !== undefined) openMessageFromSpecial(res)
|
||||
})
|
||||
}
|
||||
|
||||
function getName (
|
||||
a: Attachment,
|
||||
personAccountByIdStore: IdMap<PersonAccount>,
|
||||
personByIdStore: IdMap<Person>
|
||||
): string | undefined {
|
||||
const acc = personAccountByIdStore.get(a.modifiedBy as Ref<PersonAccount>)
|
||||
if (acc !== undefined) {
|
||||
const emp = personByIdStore.get(acc?.person)
|
||||
if (emp !== undefined) {
|
||||
return getContactName(client.getHierarchy(), emp)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ac-header full divide caption-height">
|
||||
<div class="ac-header__wrap-title">
|
||||
<span class="ac-header__title"><Label label={chunter.string.SavedItems} /></span>
|
||||
</div>
|
||||
</div>
|
||||
<Scroller>
|
||||
{#if savedMessages.length > 0 || savedAttachments.length > 0}
|
||||
{#each savedMessages as message}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="clear-mins flex-no-shrink"
|
||||
on:click={() => {
|
||||
openMessageFromSpecial(message)
|
||||
}}
|
||||
>
|
||||
<Message
|
||||
{message}
|
||||
on:openThread
|
||||
thread
|
||||
isPinned={pinnedIds.includes(message._id)}
|
||||
isSaved={savedMessagesIds.includes(message._id)}
|
||||
{savedAttachmentsIds}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{#each savedAttachments as att}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="attachmentContainer flex-no-shrink clear-mins" on:click={() => openAttachment(att)}>
|
||||
<AttachmentPreview value={att} isSaved={true} />
|
||||
<div class="label">
|
||||
<Label
|
||||
label={chunter.string.SharedBy}
|
||||
params={{
|
||||
name: getName(att, $personAccountByIdStore, $personByIdStore),
|
||||
time: getDisplayTime(att.modifiedOn)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="empty">
|
||||
<Bookmark size={'large'} />
|
||||
<div class="an-element__label header">
|
||||
<Label label={chunter.string.EmptySavedHeader} />
|
||||
</div>
|
||||
<span class="an-element__label">
|
||||
<Label label={chunter.string.EmptySavedText} />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.empty {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: inherit;
|
||||
width: 30rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: 600;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.attachmentContainer {
|
||||
padding: 2rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--highlight-hover);
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,42 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022 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 { ChunterSpace } from '@hcengineering/chunter'
|
||||
import type { Ref } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import chunter from '../plugin'
|
||||
import DmHeader from './DmHeader.svelte'
|
||||
import ChannelHeader from './ChannelHeader.svelte'
|
||||
|
||||
export let spaceId: Ref<ChunterSpace> | undefined
|
||||
export let withSearch: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const query = createQuery()
|
||||
let channel: ChunterSpace | undefined
|
||||
|
||||
$: query.query(chunter.class.ChunterSpace, { _id: spaceId }, (result) => {
|
||||
channel = result[0]
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if channel}
|
||||
{#if hierarchy.isDerived(channel._class, chunter.class.DirectMessage)}
|
||||
<DmHeader {spaceId} {withSearch} />
|
||||
{:else}
|
||||
<ChannelHeader {spaceId} />
|
||||
{/if}
|
||||
{/if}
|
@ -1,230 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import chunter, { type ChunterSpace, type Message, type ThreadMessage } from '@hcengineering/chunter'
|
||||
import contact, { Person, PersonAccount, getName } from '@hcengineering/contact'
|
||||
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import core, { FindOptions, IdMap, Ref, SortingOrder, generateId, getCurrentAccount } from '@hcengineering/core'
|
||||
import { NotificationClientImpl } from '@hcengineering/notification-resources'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import activity from '@hcengineering/activity'
|
||||
|
||||
import plugin from '../plugin'
|
||||
import ChannelPresenter from './ChannelPresenter.svelte'
|
||||
import DmPresenter from './DmPresenter.svelte'
|
||||
import MsgView from './Message.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
const messageQuery = createQuery()
|
||||
const currentPerson = (getCurrentAccount() as PersonAccount)?.person
|
||||
const currentEmployee = currentPerson !== undefined ? $personByIdStore.get(currentPerson) : undefined
|
||||
|
||||
export let savedAttachmentsIds: Ref<Attachment>[]
|
||||
export let _id: Ref<Message>
|
||||
export let showHeader = true
|
||||
export let readOnly = false
|
||||
|
||||
let parent: Message | undefined
|
||||
let commentId: Ref<ThreadMessage> = generateId()
|
||||
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const lookup = {
|
||||
_id: { attachments: attachment.class.Attachment, reactions: activity.class.Reaction },
|
||||
createBy: core.class.Account
|
||||
}
|
||||
|
||||
let showAll = false
|
||||
let total = 0
|
||||
|
||||
let showActions = false
|
||||
let showSend = false
|
||||
|
||||
$: updateQuery(_id)
|
||||
$: updateThreadQuery(_id, showAll)
|
||||
|
||||
function updateQuery (id: Ref<Message>) {
|
||||
messageQuery.query(
|
||||
plugin.class.Message,
|
||||
{
|
||||
_id: id
|
||||
},
|
||||
(res) => {
|
||||
parent = res[0]
|
||||
},
|
||||
{
|
||||
lookup
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function updateThreadQuery (id: Ref<Message>, showAll: boolean) {
|
||||
const options: FindOptions<ThreadMessage> = {
|
||||
lookup,
|
||||
sort: {
|
||||
createdOn: SortingOrder.Descending
|
||||
},
|
||||
total: true
|
||||
}
|
||||
if (!showAll) {
|
||||
options.limit = 4
|
||||
}
|
||||
query.query(
|
||||
plugin.class.ThreadMessage,
|
||||
{
|
||||
attachedTo: id
|
||||
},
|
||||
(res) => {
|
||||
total = res.total
|
||||
if (!showAll && res.total > 4) {
|
||||
comments = res.splice(0, 2).reverse()
|
||||
} else {
|
||||
comments = res.reverse()
|
||||
}
|
||||
notificationClient.read(id)
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
async function getParticipants (
|
||||
comments: ThreadMessage[],
|
||||
parent: Message | undefined,
|
||||
employees: IdMap<Person>
|
||||
): Promise<string[]> {
|
||||
const refs = new Set(comments.map((p) => p.createBy))
|
||||
if (parent !== undefined) {
|
||||
refs.add(parent.createBy)
|
||||
}
|
||||
refs.delete(getCurrentAccount()._id)
|
||||
const accounts = await client.findAll(contact.class.PersonAccount, {
|
||||
_id: { $in: Array.from(refs) as Ref<PersonAccount>[] }
|
||||
})
|
||||
const res: string[] = []
|
||||
for (const account of accounts) {
|
||||
const employee = employees.get(account.person)
|
||||
if (employee !== undefined) {
|
||||
res.push(getName(client.getHierarchy(), employee))
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function onMessage (event: CustomEvent) {
|
||||
if (parent === undefined) return
|
||||
const { message, attachments } = event.detail
|
||||
const me = getCurrentAccount()._id
|
||||
await client.addCollection(
|
||||
chunter.class.ThreadMessage,
|
||||
parent.space,
|
||||
parent._id,
|
||||
parent._class,
|
||||
'repliesCount',
|
||||
{
|
||||
content: message,
|
||||
createBy: me,
|
||||
attachments
|
||||
},
|
||||
commentId
|
||||
)
|
||||
dispatch('added', commentId)
|
||||
|
||||
commentId = generateId()
|
||||
loading = false
|
||||
}
|
||||
let comments: ThreadMessage[] = []
|
||||
|
||||
async function getChannel (_id: Ref<ChunterSpace>): Promise<ChunterSpace | undefined> {
|
||||
return await client.findOne(plugin.class.ChunterSpace, { _id })
|
||||
}
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
{#if showHeader && parent}
|
||||
<div class="flex-col ml-4 mt-4 flex-no-shrink">
|
||||
{#await getChannel(parent.space) then channel}
|
||||
{#if channel?._class === plugin.class.Channel}
|
||||
<ChannelPresenter value={channel} />
|
||||
{:else if channel}
|
||||
<DmPresenter value={channel} />
|
||||
{/if}
|
||||
{/await}
|
||||
<div class="text-sm">
|
||||
{#await getParticipants(comments, parent, $personByIdStore) then participants}
|
||||
{participants.join(', ')}
|
||||
<Label label={plugin.string.AndYou} params={{ participants: participants.length }} />
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-col content mt-2 flex-no-shrink">
|
||||
{#if parent}
|
||||
<MsgView message={parent} thread {savedAttachmentsIds} {readOnly} />
|
||||
{#if total > comments.length}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="label pb-2 pt-2 pl-8 over-underline clear-mins"
|
||||
on:click={() => {
|
||||
showAll = true
|
||||
}}
|
||||
>
|
||||
<Label label={plugin.string.ShowMoreReplies} params={{ count: total - comments.length }} />
|
||||
</div>
|
||||
{/if}
|
||||
{#each comments as comment (comment._id)}
|
||||
<MsgView message={comment} thread {savedAttachmentsIds} {readOnly} />
|
||||
{/each}
|
||||
{#if !readOnly}
|
||||
<div class="flex mr-4 ml-4 pb-4 mt-2 clear-mins">
|
||||
<div class="min-w-6">
|
||||
<Avatar size="x-small" avatar={currentEmployee?.avatar} name={currentEmployee?.name} />
|
||||
</div>
|
||||
<div class="ml-2 w-full">
|
||||
<AttachmentRefInput
|
||||
space={parent.space}
|
||||
_class={plugin.class.ThreadMessage}
|
||||
objectId={commentId}
|
||||
placeholder={chunter.string.AddCommentPlaceholder}
|
||||
{showActions}
|
||||
{showSend}
|
||||
on:message={onMessage}
|
||||
on:focus={() => {
|
||||
showSend = true
|
||||
showActions = true
|
||||
}}
|
||||
bind:loading
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label:hover {
|
||||
background-color: var(--theme-button-hovered);
|
||||
}
|
||||
</style>
|
@ -1,249 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2021 Anticrm Platform Contributors.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import { type ChunterMessage, type Message, type ThreadMessage } from '@hcengineering/chunter'
|
||||
import core, { AttachedDoc, Doc, Ref, Space, generateId, getCurrentAccount } from '@hcengineering/core'
|
||||
import { DocUpdates } from '@hcengineering/notification'
|
||||
import { NotificationClientImpl } from '@hcengineering/notification-resources'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { IconClose, Label, getCurrentResolvedLocation, navigate } from '@hcengineering/ui'
|
||||
import { afterUpdate, beforeUpdate, createEventDispatcher } from 'svelte'
|
||||
import activity from '@hcengineering/activity'
|
||||
|
||||
import chunter from '../plugin'
|
||||
import { isMessageHighlighted, messageIdForScroll, scrollAndHighLight, shouldScrollToMessage } from '../utils'
|
||||
import MsgView from './Message.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
const messageQuery = createQuery()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let _id: Ref<Message>
|
||||
export let currentSpace: Ref<Space>
|
||||
let message: Message | undefined
|
||||
let commentId: Ref<AttachedDoc> = generateId()
|
||||
|
||||
let div: HTMLDivElement | undefined
|
||||
let autoscroll: boolean = false
|
||||
let isScrollForced = false
|
||||
|
||||
beforeUpdate(() => {
|
||||
autoscroll = div !== undefined && div.offsetHeight + div.scrollTop > div.scrollHeight - 20
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
if ($shouldScrollToMessage && !$isMessageHighlighted) {
|
||||
scrollAndHighLight()
|
||||
|
||||
return
|
||||
}
|
||||
if (div && (autoscroll || isScrollForced)) {
|
||||
div.scrollTo(0, div.scrollHeight)
|
||||
isScrollForced = false
|
||||
}
|
||||
})
|
||||
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const docUpdates = notificationClient.docUpdatesStore
|
||||
|
||||
const lookup = {
|
||||
_id: { attachments: attachment.class.Attachment, reactions: activity.class.Reaction },
|
||||
createBy: core.class.Account
|
||||
}
|
||||
|
||||
const pinnedQuery = createQuery()
|
||||
let pinnedIds: Ref<ChunterMessage>[] = []
|
||||
|
||||
$: updateQueries(_id)
|
||||
|
||||
function updateQueries (id: Ref<Message>) {
|
||||
messageQuery.query(
|
||||
chunter.class.Message,
|
||||
{
|
||||
_id: id
|
||||
},
|
||||
(res) => {
|
||||
message = res[0]
|
||||
|
||||
if (!message) {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.path.length = 4
|
||||
navigate(loc)
|
||||
}
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
_id: { attachments: attachment.class.Attachment, reactions: activity.class.Reaction },
|
||||
createBy: core.class.Account
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
query.query(
|
||||
chunter.class.ThreadMessage,
|
||||
{
|
||||
attachedTo: id
|
||||
},
|
||||
(res) => {
|
||||
comments = res
|
||||
// newMessagesPos = newMessagesStart(comments, $docUpdates)
|
||||
notificationClient.read(id)
|
||||
},
|
||||
{
|
||||
lookup
|
||||
}
|
||||
)
|
||||
|
||||
pinnedQuery.query(
|
||||
chunter.class.ChunterSpace,
|
||||
{ _id: currentSpace },
|
||||
(res) => {
|
||||
pinnedIds = res[0]?.pinned ?? []
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
}
|
||||
|
||||
const savedMessagesQuery = createQuery()
|
||||
let savedMessagesIds: Ref<ChunterMessage>[] = []
|
||||
|
||||
savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
|
||||
savedMessagesIds = res.map((r) => r.attachedTo)
|
||||
})
|
||||
|
||||
const savedAttachmentsQuery = createQuery()
|
||||
let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
|
||||
savedAttachmentsIds = res.map((r) => r.attachedTo)
|
||||
})
|
||||
|
||||
async function onMessage (event: CustomEvent) {
|
||||
const { message, attachments } = event.detail
|
||||
const me = getCurrentAccount()._id
|
||||
await client.addCollection(
|
||||
chunter.class.ThreadMessage,
|
||||
currentSpace,
|
||||
_id,
|
||||
chunter.class.Message,
|
||||
'repliesCount',
|
||||
{
|
||||
content: message,
|
||||
createBy: me,
|
||||
attachments
|
||||
},
|
||||
commentId
|
||||
)
|
||||
|
||||
commentId = generateId()
|
||||
isScrollForced = true
|
||||
loading = false
|
||||
}
|
||||
let comments: ThreadMessage[] = []
|
||||
|
||||
function newMessagesStart (comments: ThreadMessage[], docUpdates: Map<Ref<Doc>, DocUpdates>): number {
|
||||
const docUpdate = docUpdates.get(_id)
|
||||
const lastView = docUpdate?.txes?.findLast((tx) => !tx.isNew)
|
||||
if (!docUpdate?.txes.some((tx) => tx.isNew)) return -1
|
||||
if (docUpdate === undefined || lastView === undefined) return -1
|
||||
for (let index = 0; index < comments.length; index++) {
|
||||
const comment = comments[index]
|
||||
if ((comment.createdOn ?? 0) >= lastView.modifiedOn) return index
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
$: markUnread(comments, $docUpdates)
|
||||
function markUnread (comments: ThreadMessage[], docUpdates: Map<Ref<Doc>, DocUpdates>) {
|
||||
const newPos = newMessagesStart(comments, docUpdates)
|
||||
if (newPos !== -1) {
|
||||
// newMessagesPos = newPos
|
||||
}
|
||||
}
|
||||
|
||||
// let newMessagesPos: number = -1
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<div class="title"><Label label={chunter.string.Thread} /></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>
|
||||
<div class="flex-col vScroll content" bind:this={div}>
|
||||
{#if message}
|
||||
<MsgView {message} thread isSaved={savedMessagesIds.includes(message._id)} {savedAttachmentsIds} />
|
||||
{#each comments as comment}
|
||||
<MsgView
|
||||
isHighlighted={$messageIdForScroll === comment._id && $isMessageHighlighted}
|
||||
message={comment}
|
||||
thread
|
||||
isPinned={pinnedIds.includes(comment._id)}
|
||||
isSaved={savedMessagesIds.includes(comment._id)}
|
||||
{savedAttachmentsIds}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="ref-input">
|
||||
<AttachmentRefInput
|
||||
space={currentSpace}
|
||||
_class={chunter.class.ThreadMessage}
|
||||
objectId={commentId}
|
||||
placeholder={chunter.string.AddCommentPlaceholder}
|
||||
on:message={onMessage}
|
||||
bind:loading
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 1.75rem 0 1rem;
|
||||
height: 4rem;
|
||||
min-height: 4rem;
|
||||
|
||||
.title {
|
||||
flex-grow: 1;
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
color: var(--caption-color);
|
||||
user-select: none;
|
||||
}
|
||||
.tool {
|
||||
margin-left: 0.75rem;
|
||||
opacity: 0.4;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ref-input {
|
||||
margin: 1.25rem 1rem;
|
||||
}
|
||||
</style>
|
@ -1,67 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import type { Message } from '@hcengineering/chunter'
|
||||
import { getCurrentAccount, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Label, Scroller } from '@hcengineering/ui'
|
||||
import chunter from '../plugin'
|
||||
import Thread from './Thread.svelte'
|
||||
|
||||
const query = createQuery()
|
||||
const me = getCurrentAccount()._id
|
||||
|
||||
let threads: Ref<Message>[] = []
|
||||
|
||||
query.query(
|
||||
chunter.class.ThreadMessage,
|
||||
{
|
||||
createBy: me
|
||||
},
|
||||
(res) => {
|
||||
const ids = new Set(res.map((c) => c.attachedTo))
|
||||
threads = Array.from(ids)
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
createdOn: SortingOrder.Descending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const savedAttachmentsQuery = createQuery()
|
||||
let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
|
||||
savedAttachmentsIds = res.map((r) => r.attachedTo)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="ac-header full divide caption-height">
|
||||
<div class="ac-header__wrap-title">
|
||||
<span class="ac-header__title"><Label label={chunter.string.Threads} /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Scroller>
|
||||
{#each threads as thread, i (thread)}
|
||||
<div class="ml-4 mr-4">
|
||||
<Thread _id={thread} {savedAttachmentsIds} />
|
||||
</div>
|
||||
{#if i < threads.length - 1}
|
||||
<div class="antiDivider" />
|
||||
{/if}
|
||||
{/each}
|
||||
</Scroller>
|
@ -1,116 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 type { Comment } from '@hcengineering/chunter'
|
||||
import type { AttachedData, TxCreateDoc } from '@hcengineering/core'
|
||||
import { getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import { AttachmentDocList, AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import { Button } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import chunter from '../../plugin'
|
||||
import { LinkPresenter } from '@hcengineering/view-resources'
|
||||
|
||||
export let tx: TxCreateDoc<Comment>
|
||||
export let value: Comment
|
||||
export let edit: boolean = false
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const editing = false
|
||||
|
||||
async function onMessage (event: CustomEvent<AttachedData<Comment>>) {
|
||||
loading = true
|
||||
try {
|
||||
const { message, attachments } = event.detail
|
||||
await client.updateCollection(
|
||||
tx.objectClass,
|
||||
tx.objectSpace,
|
||||
tx.objectId,
|
||||
value.attachedTo,
|
||||
value.attachedToClass,
|
||||
value.collection,
|
||||
{
|
||||
message,
|
||||
attachments
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
dispatch('close', false)
|
||||
}
|
||||
let refInput: AttachmentRefInput
|
||||
let loading = false
|
||||
|
||||
$: links = getLinks(value.message)
|
||||
|
||||
function getLinks (content: string): HTMLLinkElement[] {
|
||||
const parser = new DOMParser()
|
||||
const parent = parser.parseFromString(content, 'text/html').firstChild?.childNodes[1] as HTMLElement
|
||||
return parseLinks(parent.childNodes)
|
||||
}
|
||||
|
||||
function parseLinks (nodes: NodeListOf<ChildNode>): HTMLLinkElement[] {
|
||||
const res: HTMLLinkElement[] = []
|
||||
nodes.forEach((p) => {
|
||||
if (p.nodeType !== Node.TEXT_NODE) {
|
||||
if (p.nodeName === 'A') {
|
||||
res.push(p as HTMLLinkElement)
|
||||
}
|
||||
res.push(...parseLinks(p.childNodes))
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class:editing class="content-color">
|
||||
{#if edit}
|
||||
<AttachmentRefInput
|
||||
bind:loading
|
||||
bind:this={refInput}
|
||||
_class={value._class}
|
||||
objectId={value._id}
|
||||
space={value.space}
|
||||
content={value.message}
|
||||
on:message={onMessage}
|
||||
showSend={false}
|
||||
{boundary}
|
||||
/>
|
||||
<div class="flex-row-center gap-2 justify-end mt-2">
|
||||
<Button
|
||||
label={chunter.string.EditCancel}
|
||||
on:click={() => {
|
||||
dispatch('close', false)
|
||||
}}
|
||||
/>
|
||||
<Button label={chunter.string.EditUpdate} accent on:click={() => refInput.submit()} />
|
||||
</div>
|
||||
{:else}
|
||||
<MessageViewer message={value.message} />
|
||||
<AttachmentDocList {value} />
|
||||
{#each links as link}
|
||||
<LinkPresenter {link} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.editing {
|
||||
border: 1px solid var(--primary-button-outline);
|
||||
}
|
||||
</style>
|
@ -1,9 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { TxCreateDoc } from '@hcengineering/core'
|
||||
import { Message } from '@hcengineering/chunter'
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
|
||||
export let tx: TxCreateDoc<Message>
|
||||
</script>
|
||||
|
||||
<MessageViewer message={tx.attributes.content} />
|
@ -21,11 +21,14 @@
|
||||
import { getLinkData, LinkData } from '@hcengineering/activity-resources'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
import chunter from '../../plugin'
|
||||
|
||||
export let message: ChatMessage
|
||||
export let person: Person | undefined
|
||||
export let viewlet: ChatMessageViewlet | undefined
|
||||
export let object: Doc | undefined
|
||||
export let parentObject: Doc | undefined
|
||||
export let skipLabel = false
|
||||
|
||||
let linkData: LinkData | undefined = undefined
|
||||
|
||||
@ -34,21 +37,21 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if viewlet?.label}
|
||||
<span class="text-sm lower"> <Label label={viewlet.label} /></span>
|
||||
{#if !skipLabel}
|
||||
<span class="text-sm lower"> <Label label={viewlet?.label ?? chunter.string.SentMessage} /></span>
|
||||
|
||||
{#if linkData}
|
||||
<span class="text-sm lower"><Label label={linkData.preposition} /></span>
|
||||
<span class="text-sm">
|
||||
<DocNavLink {object} component={linkData.panelComponent} shrink={0}>
|
||||
<DocNavLink object={linkData.object} component={linkData.panelComponent} shrink={0}>
|
||||
<span class="overflow-label select-text">{linkData.title}</span>
|
||||
</DocNavLink>
|
||||
</span>
|
||||
{#if message.isEdited}
|
||||
<span class="text-sm lower"><Label label={notification.string.Edited} /></span>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{#if message.editedOn}
|
||||
<span class="text-sm lower"><Label label={notification.string.Edited} /></span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
|
@ -15,24 +15,31 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import { Doc, generateId, getCurrentAccount } from '@hcengineering/core'
|
||||
import { Class, Doc, generateId, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import chunter, { ChatMessage, ThreadMessage } from '@hcengineering/chunter'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
|
||||
export let object: Doc
|
||||
export let chatMessage: ChatMessage | undefined = undefined
|
||||
export let shouldSaveDraft: boolean = true
|
||||
export let focusIndex: number = -1
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
export let loading = false
|
||||
export let collection: string = 'comments'
|
||||
|
||||
type MessageDraft = Pick<ChatMessage, '_id' | 'message' | 'attachments'>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
const _class = chunter.class.ChatMessage
|
||||
const hierarchy = client.getHierarchy()
|
||||
const _class: Ref<Class<ChatMessage>> = hierarchy.isDerived(object._class, activity.class.ActivityMessage)
|
||||
? chunter.class.ThreadMessage
|
||||
: chunter.class.ChatMessage
|
||||
const createdMessageQuery = createQuery()
|
||||
const account = getCurrentAccount()
|
||||
const account = getCurrentAccount() as PersonAccount
|
||||
|
||||
const draftKey = `${object._id}_${_class}`
|
||||
const draftController = new DraftController<MessageDraft>(draftKey)
|
||||
@ -47,9 +54,8 @@
|
||||
let currentMessage: MessageDraft = chatMessage ?? currentDraft ?? getDefault()
|
||||
let _id = currentMessage._id
|
||||
let inputContent = currentMessage.message
|
||||
let loading = false
|
||||
|
||||
$: createdMessageQuery.query(chunter.class.ChatMessage, { _id }, (result: ChatMessage[]) => {
|
||||
$: createdMessageQuery.query(_class, { _id }, (result: ChatMessage[]) => {
|
||||
if (result.length > 0 && _id !== chatMessage?._id) {
|
||||
// Ouch we have got comment with same id created already.
|
||||
currentMessage = getDefault()
|
||||
@ -108,17 +114,46 @@
|
||||
async function createMessage (event: CustomEvent) {
|
||||
const { message, attachments } = event.detail
|
||||
|
||||
await client.addCollection<Doc, ChatMessage>(
|
||||
_class,
|
||||
object.space,
|
||||
object._id,
|
||||
object._class,
|
||||
'comments',
|
||||
{ message, attachments },
|
||||
_id,
|
||||
Date.now(),
|
||||
account._id
|
||||
)
|
||||
if (_class === chunter.class.ThreadMessage) {
|
||||
const parentMessage = object as ActivityMessage
|
||||
|
||||
await client.addCollection<ActivityMessage, ThreadMessage>(
|
||||
chunter.class.ThreadMessage,
|
||||
parentMessage.space,
|
||||
parentMessage._id,
|
||||
parentMessage._class,
|
||||
'replies',
|
||||
{
|
||||
message,
|
||||
attachments,
|
||||
objectClass: parentMessage.attachedToClass,
|
||||
objectId: parentMessage.attachedTo
|
||||
},
|
||||
_id as Ref<ThreadMessage>,
|
||||
Date.now(),
|
||||
account._id
|
||||
)
|
||||
|
||||
await client.update(parentMessage, { lastReply: Date.now() })
|
||||
|
||||
const hasPerson = !!parentMessage.repliedPersons?.includes(account.person)
|
||||
|
||||
if (!hasPerson) {
|
||||
await client.update(parentMessage, { $push: { repliedPersons: account.person } })
|
||||
}
|
||||
} else {
|
||||
await client.addCollection<Doc, ChatMessage>(
|
||||
_class,
|
||||
object.space,
|
||||
object._id,
|
||||
object._class,
|
||||
collection,
|
||||
{ message, attachments },
|
||||
_id,
|
||||
Date.now(),
|
||||
account._id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function editMessage (event: CustomEvent) {
|
||||
@ -126,7 +161,7 @@
|
||||
return
|
||||
}
|
||||
const { message, attachments } = event.detail
|
||||
await client.update(chatMessage, { message, attachments, isEdited: true })
|
||||
await client.update(chatMessage, { message, attachments, editedOn: Date.now() })
|
||||
}
|
||||
export function submit (): void {
|
||||
inputRef.submit()
|
||||
|
@ -24,10 +24,10 @@
|
||||
import view from '@hcengineering/view'
|
||||
import activity, { DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import { ActivityMessageTemplate } from '@hcengineering/activity-resources'
|
||||
import chunter, { ChatMessage, ChatMessageViewlet } from '@hcengineering/chunter'
|
||||
|
||||
import ChatMessageHeader from './ChatMessageHeader.svelte'
|
||||
import ChatMessageInput from './ChatMessageInput.svelte'
|
||||
import chunter, { ChatMessage, ChatMessageViewlet } from '@hcengineering/chunter'
|
||||
|
||||
export let value: ChatMessage | undefined
|
||||
export let showNotify: boolean = false
|
||||
@ -35,8 +35,12 @@
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let withActions: boolean = true
|
||||
export let showEmbedded = false
|
||||
export let hideReplies = false
|
||||
export let skipLabel = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -58,7 +62,7 @@
|
||||
$: value &&
|
||||
viewletQuery.query(
|
||||
chunter.class.ChatMessageViewlet,
|
||||
{ objectClass: value.attachedToClass },
|
||||
{ objectClass: value.attachedToClass, messageClass: value._class },
|
||||
(result: ChatMessageViewlet[]) => {
|
||||
viewlet = result[0]
|
||||
}
|
||||
@ -131,13 +135,14 @@
|
||||
{
|
||||
label: activity.string.Edit,
|
||||
icon: IconEdit,
|
||||
group: 'edit',
|
||||
action: handleEditAction
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
|
||||
$: excludedActions = isOwn ? [] : [chunter.action.DeleteChatMessage]
|
||||
$: excludedActions = []
|
||||
let refInput: ChatMessageInput
|
||||
</script>
|
||||
|
||||
@ -153,12 +158,15 @@
|
||||
{isSelected}
|
||||
{shouldScroll}
|
||||
{embedded}
|
||||
{hasActionsMenu}
|
||||
{withActions}
|
||||
{actions}
|
||||
{showEmbedded}
|
||||
{hideReplies}
|
||||
{onClick}
|
||||
{onReply}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<ChatMessageHeader {object} {parentObject} message={value} {viewlet} {person} />
|
||||
<ChatMessageHeader {object} {parentObject} message={value} {viewlet} {person} {skipLabel} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if !isEditing}
|
||||
|
146
plugins/chunter-resources/src/components/chat/Chat.svelte
Normal file
146
plugins/chunter-resources/src/components/chat/Chat.svelte
Normal file
@ -0,0 +1,146 @@
|
||||
<!--
|
||||
// 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 { createQuery } from '@hcengineering/presentation'
|
||||
import { Component, defineSeparators, getCurrentLocation, location, navigate, Separator } from '@hcengineering/ui'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import notification, { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { NavHeader } from '@hcengineering/workbench-resources'
|
||||
import { NavigatorModel, SpecialNavModel } from '@hcengineering/workbench'
|
||||
import { ActivityMessagesFilter } from '@hcengineering/activity'
|
||||
|
||||
import ChatNavigator from './navigator/ChatNavigator.svelte'
|
||||
import ChannelView from '../ChannelView.svelte'
|
||||
import DocChatPanel from './DocChatPanel.svelte'
|
||||
import { chatSpecials } from './utils'
|
||||
|
||||
export let visibleNav: boolean = true
|
||||
export let navFloat: boolean = false
|
||||
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
|
||||
|
||||
const notifyContextQuery = createQuery()
|
||||
const objectQuery = createQuery()
|
||||
|
||||
const navigatorModel: NavigatorModel = {
|
||||
spaces: [],
|
||||
specials: chatSpecials
|
||||
}
|
||||
|
||||
let selectedContextId: Ref<DocNotifyContext> | undefined = undefined
|
||||
let selectedContext: DocNotifyContext | undefined = undefined
|
||||
let filterId: Ref<ActivityMessagesFilter> | undefined = undefined
|
||||
|
||||
let object: Doc | undefined = undefined
|
||||
|
||||
let currentSpecial: SpecialNavModel | undefined
|
||||
|
||||
location.subscribe((loc) => {
|
||||
updateSpecialComponent(loc.path[3])
|
||||
updateSelectedContext(loc.path[3])
|
||||
filterId = loc.query?.filter as Ref<ActivityMessagesFilter> | undefined
|
||||
})
|
||||
|
||||
function updateSpecialComponent (id?: string): SpecialNavModel | undefined {
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
currentSpecial = navigatorModel?.specials?.find((special) => special.id === id)
|
||||
}
|
||||
|
||||
function updateSelectedContext (id?: string) {
|
||||
selectedContextId = id as Ref<DocNotifyContext> | undefined
|
||||
|
||||
if (selectedContext && selectedContextId !== selectedContext._id) {
|
||||
selectedContext = undefined
|
||||
}
|
||||
}
|
||||
|
||||
defineSeparators('chat', [
|
||||
{ minSize: 20, maxSize: 40, size: 30, float: 'navigator' },
|
||||
{ size: 'auto', minSize: 30, maxSize: 'auto', float: undefined }
|
||||
])
|
||||
|
||||
$: selectedContextId &&
|
||||
notifyContextQuery.query(
|
||||
notification.class.DocNotifyContext,
|
||||
{ _id: selectedContextId },
|
||||
(res: DocNotifyContext[]) => {
|
||||
selectedContext = res[0]
|
||||
}
|
||||
)
|
||||
|
||||
$: selectedContext !== undefined &&
|
||||
objectQuery.query(selectedContext.attachedToClass, { _id: selectedContext.attachedTo }, (res: Doc[]) => {
|
||||
object = res[0]
|
||||
})
|
||||
|
||||
$: if (selectedContext) {
|
||||
console.log({ selectedContext: selectedContext.attachedToClass })
|
||||
}
|
||||
|
||||
$: isDocChatOpened =
|
||||
selectedContext !== undefined &&
|
||||
![chunter.class.Channel, chunter.class.DirectMessage].includes(selectedContext.attachedToClass)
|
||||
</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">
|
||||
{#if !isDocChatOpened}
|
||||
<NavHeader label={chunter.string.Chat} />
|
||||
<ChatNavigator {selectedContextId} {currentSpecial} />
|
||||
{:else if object}
|
||||
<DocChatPanel {object} {filterId} />
|
||||
{/if}
|
||||
</div>
|
||||
<Separator name="chat" float={navFloat ? 'navigator' : true} index={0} />
|
||||
</div>
|
||||
<Separator name="chat" float={navFloat} index={0} />
|
||||
{/if}
|
||||
|
||||
<div class="antiPanel-component filled w-full">
|
||||
{#if currentSpecial}
|
||||
<Component
|
||||
is={currentSpecial.component}
|
||||
props={{
|
||||
model: navigatorModel,
|
||||
...currentSpecial.componentProps,
|
||||
visibleNav,
|
||||
navFloat,
|
||||
appsDirection
|
||||
}}
|
||||
on:action={(e) => {
|
||||
if (e?.detail) {
|
||||
const loc = getCurrentLocation()
|
||||
loc.query = { ...loc.query, ...e.detail }
|
||||
navigate(loc)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if selectedContext && object}
|
||||
{#key selectedContext._id}
|
||||
<ChannelView notifyContext={selectedContext} {object} {filterId} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,163 @@
|
||||
<!--
|
||||
// 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, Mixin, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { ActionIcon, getCurrentResolvedLocation, Label, navigate } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { DocAttributeBar, DocNavLink, getDocLinkTitle, getDocMixins, openDoc } from '@hcengineering/view-resources'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { SpecialElement } from '@hcengineering/workbench-resources'
|
||||
import activity, { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity'
|
||||
import { combineActivityMessages } from '@hcengineering/activity-resources'
|
||||
|
||||
import chunter from '../../plugin'
|
||||
|
||||
export let object: Doc
|
||||
export let filterId: Ref<ActivityMessagesFilter> = activity.ids.AllFilter
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const activityMessagesQuery = createQuery()
|
||||
|
||||
$: clazz = hierarchy.getClass(object._class)
|
||||
$: objectChatPanel = hierarchy.classHierarchyMixin(object._class, chunter.mixin.ObjectChatPanel)
|
||||
|
||||
let mixins: Array<Mixin<Doc>> = []
|
||||
let linkTitle: string | undefined = undefined
|
||||
|
||||
let activityMessages: ActivityMessage[] = []
|
||||
let filters: ActivityMessagesFilter[] = []
|
||||
|
||||
$: mixins = getDocMixins(object)
|
||||
|
||||
$: getDocLinkTitle(client, object._id, object._class, object).then((res) => {
|
||||
linkTitle = res
|
||||
})
|
||||
|
||||
$: activityMessagesQuery.query(activity.class.ActivityMessage, { attachedTo: object._id }, (res) => {
|
||||
activityMessages = combineActivityMessages(res)
|
||||
})
|
||||
|
||||
client
|
||||
.findAll(activity.class.ActivityMessagesFilter, {}, { sort: { position: SortingOrder.Ascending } })
|
||||
.then((res) => {
|
||||
filters = res
|
||||
})
|
||||
|
||||
function handleFilterSelected (filter: ActivityMessagesFilter) {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.query = { filter: filter._id }
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
async function getMessagesCount (
|
||||
filter: ActivityMessagesFilter,
|
||||
activityMessages: ActivityMessage[]
|
||||
): Promise<number> {
|
||||
if (filter._id === activity.ids.AllFilter) {
|
||||
return activityMessages.length
|
||||
}
|
||||
|
||||
const filterFn = await getResource(filter.filter)
|
||||
const filteredMessages = activityMessages.filter((message) => filterFn(message, object._class))
|
||||
|
||||
return filteredMessages.length
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="popupPanel panel">
|
||||
<div class="actions">
|
||||
<ActionIcon
|
||||
icon={view.icon.Open}
|
||||
size="medium"
|
||||
action={() => {
|
||||
openDoc(client.getHierarchy(), object)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<div class="identifier">
|
||||
<Label label={clazz.label} />
|
||||
{#if linkTitle}
|
||||
•
|
||||
<DocNavLink {object}>
|
||||
{linkTitle}
|
||||
</DocNavLink>
|
||||
{/if}
|
||||
</div>
|
||||
{#if objectChatPanel}
|
||||
{#await getResource(objectChatPanel.titleProvider) then getTitle}
|
||||
<div class="title overflow-label">
|
||||
{getTitle(object)}
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<DocAttributeBar {object} {mixins} ignoreKeys={objectChatPanel?.ignoreKeys ?? []} showHeader={false} />
|
||||
|
||||
{#each filters as filter}
|
||||
{#key filter._id}
|
||||
{#await getMessagesCount(filter, activityMessages) then count}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#if filter._id === activity.ids.AllFilter || count > 0}
|
||||
<div
|
||||
on:click={() => {
|
||||
handleFilterSelected(filter)
|
||||
}}
|
||||
>
|
||||
<SpecialElement label={filter.label} selected={filterId === filter._id} notifications={count} />
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{/key}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
margin: 0.75rem;
|
||||
padding: 0 0.75rem;
|
||||
color: var(--theme-content-color);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.identifier {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
color: var(--theme-halfcontent-color);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin: 0.75rem;
|
||||
padding: 0 0.75rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
// 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
|
||||
@ -15,36 +15,43 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { IconFolder, EditBox, ToggleWithLabel, Grid } from '@hcengineering/ui'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
import presentation, { getClient, SpaceCreateCard } from '@hcengineering/presentation'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
import chunter from '../plugin'
|
||||
import core, { getCurrentAccount } from '@hcengineering/core'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
import chunter from '../../../plugin'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let isPrivate: boolean = false
|
||||
let name: string = ''
|
||||
|
||||
export function canClose (): boolean {
|
||||
return name === ''
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function createChannel () {
|
||||
const accountId = getCurrentAccount()._id
|
||||
const channelId = await client.createDoc(chunter.class.Channel, core.space.Space, {
|
||||
name,
|
||||
description: '',
|
||||
private: isPrivate,
|
||||
archived: false,
|
||||
members: [getCurrentAccount()._id]
|
||||
members: [accountId]
|
||||
})
|
||||
const notifyContextId = await client.createDoc(notification.class.DocNotifyContext, core.space.Space, {
|
||||
user: accountId,
|
||||
attachedTo: channelId,
|
||||
attachedToClass: chunter.class.Channel,
|
||||
hidden: false
|
||||
})
|
||||
const navigate = await getResource(workbench.actionImpl.Navigate)
|
||||
|
||||
await navigate([], undefined as any, {
|
||||
mode: 'space',
|
||||
space: channelId
|
||||
})
|
||||
const navigate = await getResource(chunter.actionImpl.OpenChannel)
|
||||
|
||||
await navigate(undefined, undefined, { _id: notifyContextId })
|
||||
}
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2022 Anticrm Platform Contributors.
|
||||
// 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
|
||||
@ -16,14 +16,15 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
import { DirectMessage } from '@hcengineering/chunter'
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import core, { getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { getClient, SpaceCreateCard } from '@hcengineering/presentation'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
import chunter from '../plugin'
|
||||
import { UserBoxList } from '@hcengineering/contact-resources'
|
||||
import notification, { DocNotifyContext } from '@hcengineering/notification'
|
||||
|
||||
import chunter from '../../../plugin'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
@ -33,17 +34,26 @@
|
||||
|
||||
async function createDirectMessage () {
|
||||
const employeeAccounts = await client.findAll(contact.class.PersonAccount, { person: { $in: employeeIds } })
|
||||
const accIds = [myAccId, ...employeeAccounts.filter(({ _id }) => _id !== myAccId).map(({ _id }) => _id)].sort()
|
||||
|
||||
const accIds = [myAccId, ...employeeAccounts.filter((ea) => ea._id !== myAccId).map((ea) => ea._id)].sort()
|
||||
const existingDms = await client.findAll(chunter.class.DirectMessage, {})
|
||||
const navigate = await getResource(workbench.actionImpl.Navigate)
|
||||
const existingContexts = await client.findAll<DocNotifyContext>(
|
||||
notification.class.DocNotifyContext,
|
||||
{
|
||||
user: myAccId,
|
||||
attachedToClass: chunter.class.DirectMessage
|
||||
},
|
||||
{ lookup: { attachedTo: chunter.class.DirectMessage } }
|
||||
)
|
||||
|
||||
const navigate = await getResource(chunter.actionImpl.OpenChannel)
|
||||
|
||||
for (const context of existingContexts) {
|
||||
if (deepEqual((context.$lookup?.attachedTo as DirectMessage)?.members.sort(), accIds)) {
|
||||
if (context.hidden) {
|
||||
await client.update(context, { hidden: false })
|
||||
}
|
||||
await navigate(context)
|
||||
|
||||
for (const dm of existingDms) {
|
||||
if (deepEqual(dm.members.sort(), accIds)) {
|
||||
await navigate([], undefined as any, {
|
||||
mode: 'space',
|
||||
space: dm._id
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -56,10 +66,14 @@
|
||||
members: accIds
|
||||
})
|
||||
|
||||
await navigate([], undefined as any, {
|
||||
mode: 'space',
|
||||
space: dmId
|
||||
const notifyContextId = await client.createDoc(notification.class.DocNotifyContext, core.space.Space, {
|
||||
user: myAccId,
|
||||
attachedTo: dmId,
|
||||
attachedToClass: chunter.class.DirectMessage,
|
||||
hidden: false
|
||||
})
|
||||
|
||||
await navigate(undefined, undefined, { _id: notifyContextId })
|
||||
}
|
||||
</script>
|
||||
|
@ -0,0 +1,95 @@
|
||||
<!--
|
||||
// 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 type { Doc, Ref } from '@hcengineering/core'
|
||||
import { getCurrentAccount } from '@hcengineering/core'
|
||||
import notification, { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Action, IconAdd, showPopup } from '@hcengineering/ui'
|
||||
import { TreeNode } from '@hcengineering/view-resources'
|
||||
|
||||
import { getDocByNotifyContext } from '../utils'
|
||||
import { ChatNavGroupModel } from '../types'
|
||||
import ChatNavItem from './ChatNavItem.svelte'
|
||||
|
||||
export let model: ChatNavGroupModel
|
||||
export let selectedContextId: Ref<DocNotifyContext> | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const notifyContextsQuery = createQuery()
|
||||
|
||||
let notifyContexts: DocNotifyContext[] = []
|
||||
let docByNotifyContext: Map<Ref<DocNotifyContext>, Doc> = new Map<Ref<DocNotifyContext>, Doc>()
|
||||
|
||||
notifyContextsQuery.query(
|
||||
notification.class.DocNotifyContext,
|
||||
{
|
||||
...model.query,
|
||||
hidden: false,
|
||||
user: getCurrentAccount()._id
|
||||
},
|
||||
(res: DocNotifyContext[]) => {
|
||||
notifyContexts = res
|
||||
}
|
||||
)
|
||||
|
||||
$: getDocByNotifyContext(notifyContexts).then((res) => {
|
||||
docByNotifyContext = res
|
||||
})
|
||||
|
||||
function getGroupActions (): Action[] {
|
||||
const result: Action[] = []
|
||||
|
||||
if (model.addLabel !== undefined && model.addComponent !== undefined) {
|
||||
result.push({
|
||||
label: model.addLabel,
|
||||
icon: IconAdd,
|
||||
action: async (_id: Ref<Doc>): Promise<void> => {
|
||||
dispatch('open')
|
||||
if (model.addComponent !== undefined) {
|
||||
showPopup(model.addComponent, {}, 'top')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const additionalActions = model.actions?.map(({ icon, label, action }) => ({
|
||||
icon,
|
||||
label,
|
||||
action: async (ctx: any, evt: Event) => {
|
||||
const impl = await getResource(action)
|
||||
await impl(notifyContexts, evt)
|
||||
}
|
||||
}))
|
||||
|
||||
return additionalActions ? result.concat(additionalActions) : result
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !model.hideEmpty || notifyContexts.length > 0}
|
||||
<TreeNode _id={`tree-${model.id}`} label={model.label} node actions={async () => getGroupActions()}>
|
||||
{#each notifyContexts as docNotifyContext}
|
||||
{@const doc = docByNotifyContext.get(docNotifyContext._id)}
|
||||
{#if doc}
|
||||
{#key docNotifyContext._id}
|
||||
<ChatNavItem {doc} notifyContext={docNotifyContext} {selectedContextId} />
|
||||
{/key}
|
||||
{/if}
|
||||
{/each}
|
||||
</TreeNode>
|
||||
{/if}
|
@ -0,0 +1,121 @@
|
||||
<!--
|
||||
// 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 type { Doc, Ref } from '@hcengineering/core'
|
||||
import notification, { ActivityInboxNotification, DocNotifyContext } from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Action, IconEdit } from '@hcengineering/ui'
|
||||
import {
|
||||
getActions as getContributedActions,
|
||||
getDocLinkTitle,
|
||||
getDocTitle,
|
||||
NavLink,
|
||||
TreeItem
|
||||
} from '@hcengineering/view-resources'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
import { ThreadMessage } from '@hcengineering/chunter'
|
||||
|
||||
import chunter from '../../../plugin'
|
||||
import { getChannelIcon } from '../../../utils'
|
||||
|
||||
export let doc: Doc
|
||||
export let notifyContext: DocNotifyContext
|
||||
export let selectedContextId: Ref<DocNotifyContext> | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const notificationClient = InboxNotificationsClientImpl.getClient()
|
||||
|
||||
const threadMessagesQuery = createQuery()
|
||||
|
||||
let threadMessages: ThreadMessage[] = []
|
||||
|
||||
$: inboxNotificationsStore = notificationClient.activityInboxNotifications
|
||||
|
||||
$: threadMessagesQuery.query(
|
||||
chunter.class.ThreadMessage,
|
||||
{
|
||||
objectId: doc._id
|
||||
},
|
||||
(res) => {
|
||||
threadMessages = res
|
||||
}
|
||||
)
|
||||
|
||||
function hasNewMessages (
|
||||
context: DocNotifyContext,
|
||||
notifications: ActivityInboxNotification[],
|
||||
threadMessages: ThreadMessage[]
|
||||
) {
|
||||
const { lastViewedTimestamp = 0, lastUpdateTimestamp = 0 } = context
|
||||
|
||||
if (lastViewedTimestamp < lastUpdateTimestamp) {
|
||||
return true
|
||||
}
|
||||
|
||||
const threadMessagesIds = threadMessages.map(({ _id }) => _id) as Ref<ActivityMessage>[]
|
||||
|
||||
return notifications.some(({ attachedTo, isViewed }) => threadMessagesIds.includes(attachedTo) && !isViewed)
|
||||
}
|
||||
|
||||
async function getItemActions (docNotifyContext: DocNotifyContext): Promise<Action[]> {
|
||||
const result = []
|
||||
|
||||
const actions = await getContributedActions(client, docNotifyContext, notification.class.DocNotifyContext)
|
||||
|
||||
for (const action of actions) {
|
||||
const { visibilityTester } = action
|
||||
const getIsVisible = visibilityTester && (await getResource(visibilityTester))
|
||||
const isVisible = getIsVisible ? await getIsVisible(docNotifyContext) : true
|
||||
|
||||
if (!isVisible) {
|
||||
continue
|
||||
}
|
||||
|
||||
result.push({
|
||||
icon: action.icon ?? IconEdit,
|
||||
label: action.label,
|
||||
action: async (ctx: any, evt: Event) => {
|
||||
const impl = await getResource(action.action)
|
||||
await impl(docNotifyContext, evt, action.actionProps)
|
||||
}
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function getChannelName (doc: Doc): Promise<string | undefined> {
|
||||
if (doc._class === chunter.class.DirectMessage) {
|
||||
return await getDocTitle(client, doc._id, doc._class, doc)
|
||||
}
|
||||
return await getDocLinkTitle(client, doc._id, doc._class, doc)
|
||||
}
|
||||
</script>
|
||||
|
||||
<NavLink space={notifyContext._id}>
|
||||
{#await getChannelName(doc) then name}
|
||||
<TreeItem
|
||||
_id={notifyContext._id}
|
||||
title={name ?? chunter.string.Channel}
|
||||
selected={selectedContextId === notifyContext._id}
|
||||
icon={getChannelIcon(doc)}
|
||||
iconProps={{ value: doc }}
|
||||
showNotify={hasNewMessages(notifyContext, $inboxNotificationsStore, threadMessages)}
|
||||
actions={async () => await getItemActions(notifyContext)}
|
||||
indent
|
||||
/>
|
||||
{/await}
|
||||
</NavLink>
|
@ -0,0 +1,75 @@
|
||||
<!--
|
||||
// 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 { Ref } from '@hcengineering/core'
|
||||
import { Scroller } from '@hcengineering/ui'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { SpecialNavModel } from '@hcengineering/workbench'
|
||||
import { NavLink } from '@hcengineering/view-resources'
|
||||
import { SpecialElement, TreeSeparator } from '@hcengineering/workbench-resources'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
|
||||
import ChatNavGroup from './ChatNavGroup.svelte'
|
||||
import { chatNavGroups, chatSpecials } from '../utils'
|
||||
|
||||
export let selectedContextId: Ref<DocNotifyContext> | undefined
|
||||
export let currentSpecial: SpecialNavModel | undefined
|
||||
|
||||
const notificationClient = InboxNotificationsClientImpl.getClient()
|
||||
|
||||
let notifyContexts: DocNotifyContext[] = []
|
||||
|
||||
notificationClient.docNotifyContexts.subscribe((res) => {
|
||||
notifyContexts = res
|
||||
})
|
||||
|
||||
async function isSpecialVisible (special: SpecialNavModel, docNotifyContexts: DocNotifyContext[]) {
|
||||
if (special.visibleIf === undefined) {
|
||||
return true
|
||||
}
|
||||
|
||||
const getIsVisible = await getResource(special.visibleIf)
|
||||
|
||||
return await getIsVisible(docNotifyContexts as any)
|
||||
}
|
||||
|
||||
const menuSelection: boolean = false
|
||||
</script>
|
||||
|
||||
<!--TODO: hasSpaceBrowser-->
|
||||
<Scroller shrink>
|
||||
{#each chatSpecials as special, row}
|
||||
{#if row > 0 && chatSpecials[row].position !== chatSpecials[row - 1].position}
|
||||
<TreeSeparator line />
|
||||
{/if}
|
||||
{#await isSpecialVisible(special, notifyContexts) then isVisible}
|
||||
{#if isVisible}
|
||||
<NavLink space={special.id}>
|
||||
<SpecialElement
|
||||
label={special.label}
|
||||
icon={special.icon}
|
||||
selected={menuSelection ? false : special.id === currentSpecial?.id}
|
||||
/>
|
||||
</NavLink>
|
||||
{/if}
|
||||
{/await}
|
||||
{/each}
|
||||
|
||||
{#each chatNavGroups as model}
|
||||
<ChatNavGroup {selectedContextId} {model} on:open />
|
||||
{/each}
|
||||
<div class="antiNav-space" />
|
||||
</Scroller>
|
@ -0,0 +1,262 @@
|
||||
<!--
|
||||
// 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 {
|
||||
Class,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
getCurrentAccount,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
SortingQuery,
|
||||
Space
|
||||
} from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
AnyComponent,
|
||||
Button,
|
||||
getCurrentResolvedLocation,
|
||||
Icon,
|
||||
Label,
|
||||
navigate,
|
||||
Scroller,
|
||||
SearchEdit,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { FilterBar, FilterButton, SpacePresenter } from '@hcengineering/view-resources'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
|
||||
import { getChannelIcon } from '../../../utils'
|
||||
|
||||
export let _class: Ref<Class<Space>>
|
||||
export let label: IntlString
|
||||
export let createItemDialog: AnyComponent | undefined = undefined
|
||||
export let createItemLabel: IntlString = presentation.string.Create
|
||||
export let withHeader: boolean = true
|
||||
export let withFilterButton: boolean = true
|
||||
export let search: string = ''
|
||||
|
||||
const me = getCurrentAccount()._id
|
||||
const client = getClient()
|
||||
const spaceQuery = createQuery()
|
||||
const sort: SortingQuery<Space> = {
|
||||
name: SortingOrder.Ascending
|
||||
}
|
||||
let searchQuery: DocumentQuery<Space>
|
||||
let resultQuery: DocumentQuery<Space>
|
||||
|
||||
let spaces: Space[] = []
|
||||
|
||||
$: updateSearchQuery(search)
|
||||
$: update(sort, resultQuery)
|
||||
|
||||
async function update (sort: SortingQuery<Space>, resultQuery: DocumentQuery<Space>): Promise<void> {
|
||||
const options: FindOptions<Space> = {
|
||||
sort
|
||||
}
|
||||
|
||||
spaceQuery.query(
|
||||
_class,
|
||||
{
|
||||
...resultQuery
|
||||
},
|
||||
(res) => {
|
||||
spaces = res
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
function updateSearchQuery (search: string): void {
|
||||
searchQuery = search.length ? { $search: search } : {}
|
||||
}
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
showPopup(createItemDialog as AnyComponent, {}, 'middle')
|
||||
}
|
||||
|
||||
async function join (space: Space): Promise<void> {
|
||||
if (space.members.includes(me)) return
|
||||
await client.update(space, {
|
||||
$push: {
|
||||
members: me
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function leave (space: Space): Promise<void> {
|
||||
if (!space.members.includes(me)) return
|
||||
await client.update(space, {
|
||||
$pull: {
|
||||
members: me
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function view (space: Space): Promise<void> {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.path[3] = space._id
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if withHeader}
|
||||
<div class="ac-header full divide">
|
||||
<div class="ac-header__wrap-title">
|
||||
<span class="ac-header__title"><Label {label} /></span>
|
||||
</div>
|
||||
{#if createItemDialog}
|
||||
<div class="mb-1 clear-mins">
|
||||
<Button
|
||||
label={createItemLabel}
|
||||
kind={'primary'}
|
||||
size={'medium'}
|
||||
on:click={(ev) => {
|
||||
showCreateDialog(ev)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="ac-header full divide search-start">
|
||||
<div class="ac-header-full small-gap">
|
||||
<SearchEdit
|
||||
bind:value={search}
|
||||
on:change={() => {
|
||||
updateSearchQuery(search)
|
||||
update(sort, resultQuery)
|
||||
}}
|
||||
/>
|
||||
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
|
||||
<div class="buttons-divider" />
|
||||
{#if withFilterButton}
|
||||
<FilterButton {_class} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if withFilterButton}
|
||||
<div class="ac-header full divide">
|
||||
<div class="ac-header-full small-gap">
|
||||
<FilterButton {_class} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<FilterBar {_class} query={searchQuery} space={undefined} on:change={(e) => (resultQuery = e.detail)} />
|
||||
<Scroller padding={'2.5rem'}>
|
||||
<div class="spaces-container">
|
||||
{#each spaces as space (space._id)}
|
||||
{@const icon = getChannelIcon(space)}
|
||||
{@const joined = space.members.includes(me)}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div class="item flex-between" tabindex="0">
|
||||
<div class="flex-col clear-mins">
|
||||
<div class="fs-title flex-row-center">
|
||||
{#if icon}
|
||||
<div class="icon"><Icon {icon} size={'small'} /></div>
|
||||
{/if}
|
||||
<SpacePresenter value={space} />
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
{#if joined}
|
||||
<Label label={workbench.string.Joined} />
|
||||
·
|
||||
{/if}
|
||||
{space.members.length}
|
||||
·
|
||||
{space.description}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tools flex-row-center gap-2">
|
||||
{#if joined}
|
||||
<Button
|
||||
size={'x-large'}
|
||||
label={workbench.string.Leave}
|
||||
on:click={async () => {
|
||||
await leave(space)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
size={'x-large'}
|
||||
label={workbench.string.View}
|
||||
on:click={async () => {
|
||||
await view(space)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size={'x-large'}
|
||||
kind={'primary'}
|
||||
label={workbench.string.Join}
|
||||
on:click={async () => {
|
||||
await join(space)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if createItemDialog}
|
||||
<div class="flex-center mt-10">
|
||||
<Button
|
||||
size={'x-large'}
|
||||
kind={'primary'}
|
||||
label={createItemLabel}
|
||||
on:click={(ev) => {
|
||||
showCreateDialog(ev)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.spaces-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--theme-list-border-color);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
.item {
|
||||
padding: 1rem 0.75rem;
|
||||
color: var(--theme-caption-color);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
margin-right: 0.375rem;
|
||||
color: var(--theme-trans-color);
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--theme-divider-color);
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--highlight-hover);
|
||||
|
||||
.icon {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.tools {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.tools {
|
||||
position: relative;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,19 +1,34 @@
|
||||
<!--
|
||||
// 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 attachment from '@hcengineering/attachment'
|
||||
import { FileBrowser } from '@hcengineering/attachment-resources'
|
||||
import { AnySvelteComponent, Button } from '@hcengineering/ui'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
import { SpaceBrowser } from '@hcengineering/workbench-resources'
|
||||
import Header from './Header.svelte'
|
||||
import contact from '@hcengineering/contact-resources/src/plugin'
|
||||
import { EmployeeBrowser } from '@hcengineering/contact-resources'
|
||||
import { userSearch } from '../index'
|
||||
import plugin from '../plugin'
|
||||
import { SearchType } from '../utils'
|
||||
import MessagesBrowser from './MessagesBrowser.svelte'
|
||||
import { FilterButton } from '@hcengineering/view-resources'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
|
||||
import { userSearch } from '../../../index'
|
||||
import { SearchType } from '../../../utils'
|
||||
import plugin from '../../../plugin'
|
||||
import Header from '../../Header.svelte'
|
||||
|
||||
let userSearch_: string = ''
|
||||
userSearch.subscribe((v) => (userSearch_ = v))
|
||||
|
||||
@ -25,7 +40,7 @@
|
||||
filterClass?: Ref<Class<Doc>>
|
||||
props?: Record<string, any>
|
||||
}[] = [
|
||||
{ searchType: SearchType.Messages, component: MessagesBrowser, filterClass: plugin.class.ChunterMessage },
|
||||
{ searchType: SearchType.Messages, component: MessagesBrowser },
|
||||
{
|
||||
searchType: SearchType.Channels,
|
||||
component: SpaceBrowser,
|
@ -0,0 +1,93 @@
|
||||
<!--
|
||||
// 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, { ChatMessage } from '@hcengineering/chunter'
|
||||
import { DocumentQuery, SortingOrder } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Label, Scroller, SearchEdit } from '@hcengineering/ui'
|
||||
import { FilterBar } from '@hcengineering/view-resources'
|
||||
import { ActivityMessagePresenter } from '@hcengineering/activity-resources'
|
||||
|
||||
import plugin from '../../../plugin'
|
||||
import { openMessageFromSpecial } from '../../../utils'
|
||||
|
||||
export let withHeader: boolean = true
|
||||
export let search: string = ''
|
||||
|
||||
let searchQuery: DocumentQuery<ChatMessage> = { $search: search }
|
||||
|
||||
function updateSearchQuery (search: string): void {
|
||||
searchQuery = { $search: search }
|
||||
}
|
||||
|
||||
$: updateSearchQuery(search)
|
||||
|
||||
const client = getClient()
|
||||
let messages: ChatMessage[] = []
|
||||
|
||||
let resultQuery: DocumentQuery<ChatMessage> = { ...searchQuery }
|
||||
|
||||
async function updateMessages (resultQuery: DocumentQuery<ChatMessage>) {
|
||||
messages = await client.findAll(
|
||||
chunter.class.ChatMessage,
|
||||
{
|
||||
...resultQuery
|
||||
},
|
||||
{
|
||||
sort: { createdOn: SortingOrder.Descending },
|
||||
limit: 100
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$: updateMessages(resultQuery)
|
||||
</script>
|
||||
|
||||
{#if withHeader}
|
||||
<div class="ac-header full divide">
|
||||
<div class="ac-header__wrap-title">
|
||||
<span class="ac-header__title"><Label label={plugin.string.MessagesBrowser} /></span>
|
||||
</div>
|
||||
<SearchEdit
|
||||
value={search}
|
||||
on:change={() => {
|
||||
updateSearchQuery(search)
|
||||
updateMessages(resultQuery)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<FilterBar
|
||||
_class={chunter.class.ChatMessage}
|
||||
space={undefined}
|
||||
query={searchQuery}
|
||||
on:change={(e) => (resultQuery = e.detail)}
|
||||
/>
|
||||
{#if messages.length > 0}
|
||||
<Scroller>
|
||||
{#each messages as message}
|
||||
<ActivityMessagePresenter
|
||||
value={message}
|
||||
onClick={() => {
|
||||
openMessageFromSpecial(message)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</Scroller>
|
||||
{:else}
|
||||
<div class="flex-center h-full text-lg">
|
||||
<Label label={plugin.string.NoResults} />
|
||||
</div>
|
||||
{/if}
|
@ -0,0 +1,164 @@
|
||||
<!--
|
||||
// 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 attachment, { Attachment, SavedAttachments } from '@hcengineering/attachment'
|
||||
import { AttachmentPreview } from '@hcengineering/attachment-resources'
|
||||
import { Person, PersonAccount, getName as getContactName } from '@hcengineering/contact'
|
||||
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { getDisplayTime, IdMap, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Icon, Label, Scroller } from '@hcengineering/ui'
|
||||
import activity, { ActivityMessage, SavedMessage } from '@hcengineering/activity'
|
||||
import { ActivityMessagePresenter } from '@hcengineering/activity-resources'
|
||||
|
||||
import chunter from '../../../plugin'
|
||||
import { openMessageFromSpecial } from '../../../utils'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let savedMessages: WithLookup<SavedMessage>[] = []
|
||||
let savedAttachments: WithLookup<SavedAttachments>[] = []
|
||||
|
||||
const savedMessagesQuery = createQuery()
|
||||
const savedAttachmentsQuery = createQuery()
|
||||
|
||||
savedMessagesQuery.query(
|
||||
activity.class.SavedMessage,
|
||||
{},
|
||||
(res) => {
|
||||
savedMessages = res
|
||||
},
|
||||
{ lookup: { attachedTo: activity.class.ActivityMessage } }
|
||||
)
|
||||
|
||||
savedAttachmentsQuery.query(
|
||||
attachment.class.SavedAttachments,
|
||||
{},
|
||||
(res) => {
|
||||
savedAttachments = res
|
||||
},
|
||||
{ lookup: { attachedTo: attachment.class.Attachment } }
|
||||
)
|
||||
|
||||
async function openAttachment (attach?: Attachment) {
|
||||
if (attach === undefined) {
|
||||
return
|
||||
}
|
||||
const messageId: Ref<ActivityMessage> = attach.attachedTo as Ref<ActivityMessage>
|
||||
await client.findOne(activity.class.ActivityMessage, { _id: messageId }).then((res) => {
|
||||
if (res !== undefined) {
|
||||
openMessageFromSpecial(res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getName (
|
||||
a: Attachment,
|
||||
personAccountByIdStore: IdMap<PersonAccount>,
|
||||
personByIdStore: IdMap<Person>
|
||||
): string | undefined {
|
||||
const acc = personAccountByIdStore.get(a.modifiedBy as Ref<PersonAccount>)
|
||||
if (acc !== undefined) {
|
||||
const emp = personByIdStore.get(acc?.person)
|
||||
if (emp !== undefined) {
|
||||
return getContactName(client.getHierarchy(), emp)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ac-header full divide caption-height">
|
||||
<div class="ac-header__wrap-title">
|
||||
<span class="ac-header__title"><Label label={chunter.string.SavedItems} /></span>
|
||||
</div>
|
||||
</div>
|
||||
<Scroller>
|
||||
{#if savedMessages.length > 0 || savedAttachments.length > 0}
|
||||
{#each savedMessages as message}
|
||||
{#if message.$lookup?.attachedTo}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
<ActivityMessagePresenter
|
||||
value={message.$lookup?.attachedTo}
|
||||
onClick={() => {
|
||||
openMessageFromSpecial(message.$lookup?.attachedTo)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each savedAttachments as attach}
|
||||
{#if attach.$lookup?.attachedTo}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="attachmentContainer flex-no-shrink clear-mins"
|
||||
on:click={() => openAttachment(attach.$lookup?.attachedTo)}
|
||||
>
|
||||
<AttachmentPreview value={attach.$lookup.attachedTo} isSaved={true} />
|
||||
<div class="label">
|
||||
<Label
|
||||
label={chunter.string.SharedBy}
|
||||
params={{
|
||||
name: getName(attach.$lookup.attachedTo, $personAccountByIdStore, $personByIdStore),
|
||||
time: getDisplayTime(attach.modifiedOn)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="empty">
|
||||
<Icon icon={activity.icon.Bookmark} size="large" />
|
||||
<div class="an-element__label header">
|
||||
<Label label={chunter.string.EmptySavedHeader} />
|
||||
</div>
|
||||
<span class="an-element__label">
|
||||
<Label label={chunter.string.EmptySavedText} />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.empty {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: inherit;
|
||||
width: 30rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: 600;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.attachmentContainer {
|
||||
padding: 2rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--highlight-hover);
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,55 @@
|
||||
<!--
|
||||
// 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 { getCurrentAccount } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Label, Scroller } from '@hcengineering/ui'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import { ActivityMessagePresenter } from '@hcengineering/activity-resources'
|
||||
|
||||
import chunter from '../../../plugin'
|
||||
|
||||
const threadsQuery = createQuery()
|
||||
const me = getCurrentAccount() as PersonAccount
|
||||
|
||||
let threads: ActivityMessage[] = []
|
||||
|
||||
$: threadsQuery.query(
|
||||
activity.class.ActivityMessage,
|
||||
{
|
||||
replies: { $gte: 1 }
|
||||
},
|
||||
(res) => {
|
||||
threads = res.filter(
|
||||
({ createdBy, repliedPersons }) => createdBy === me._id || repliedPersons?.includes(me.person)
|
||||
)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="ac-header full divide caption-height">
|
||||
<div class="ac-header__wrap-title">
|
||||
<span class="ac-header__title"><Label label={chunter.string.Threads} /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Scroller>
|
||||
{#each threads as thread}
|
||||
<div class="ml-4 mr-4">
|
||||
<ActivityMessagePresenter value={thread} />
|
||||
</div>
|
||||
{/each}
|
||||
</Scroller>
|
33
plugins/chunter-resources/src/components/chat/types.ts
Normal file
33
plugins/chunter-resources/src/components/chat/types.ts
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
import { type Asset, type IntlString } from '@hcengineering/platform'
|
||||
import { type DocumentQuery } from '@hcengineering/core'
|
||||
import { type DocNotifyContext } from '@hcengineering/notification'
|
||||
import { type ViewAction } from '@hcengineering/view'
|
||||
import { type AnyComponent } from '@hcengineering/ui'
|
||||
|
||||
export interface ChatNavGroupModel {
|
||||
id: string
|
||||
label?: IntlString
|
||||
addLabel?: IntlString
|
||||
addComponent?: AnyComponent
|
||||
query?: DocumentQuery<DocNotifyContext>
|
||||
hideEmpty?: boolean
|
||||
actions?: Array<{
|
||||
icon: Asset
|
||||
label: IntlString
|
||||
action: ViewAction
|
||||
}>
|
||||
}
|
178
plugins/chunter-resources/src/components/chat/utils.ts
Normal file
178
plugins/chunter-resources/src/components/chat/utils.ts
Normal file
@ -0,0 +1,178 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
import notification, { type DocNotifyContext } from '@hcengineering/notification'
|
||||
import { type Doc, type Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
import view from '@hcengineering/view'
|
||||
import workbench, { type SpecialNavModel } from '@hcengineering/workbench'
|
||||
import attachment from '@hcengineering/attachment'
|
||||
import activity from '@hcengineering/activity'
|
||||
|
||||
import { type ChatNavGroupModel } from './types'
|
||||
import chunter from '../../plugin'
|
||||
|
||||
export const chatSpecials: SpecialNavModel[] = [
|
||||
{
|
||||
id: 'channelBrowser',
|
||||
component: workbench.component.SpaceBrowser,
|
||||
icon: chunter.icon.ChannelBrowser,
|
||||
label: chunter.string.ChannelBrowser,
|
||||
position: 'top',
|
||||
componentProps: {
|
||||
_class: chunter.class.Channel,
|
||||
label: chunter.string.ChannelBrowser,
|
||||
createItemDialog: chunter.component.CreateChannel,
|
||||
createItemLabel: chunter.string.CreateChannel
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'fileBrowser',
|
||||
label: attachment.string.FileBrowser,
|
||||
icon: attachment.icon.FileBrowser,
|
||||
component: attachment.component.FileBrowser,
|
||||
position: 'top',
|
||||
componentProps: {
|
||||
requestedSpaceClasses: [chunter.class.Channel, chunter.class.DirectMessage]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'threads',
|
||||
label: chunter.string.Threads,
|
||||
icon: chunter.icon.Thread,
|
||||
component: chunter.component.Threads,
|
||||
position: 'top'
|
||||
},
|
||||
{
|
||||
id: 'saved',
|
||||
label: chunter.string.SavedItems,
|
||||
icon: activity.icon.Bookmark,
|
||||
position: 'bottom',
|
||||
component: chunter.component.SavedMessages
|
||||
},
|
||||
{
|
||||
id: 'archive',
|
||||
component: workbench.component.Archive,
|
||||
icon: view.icon.Archive,
|
||||
label: workbench.string.Archive,
|
||||
position: 'bottom',
|
||||
componentProps: {
|
||||
_class: notification.class.DocNotifyContext,
|
||||
config: [
|
||||
{ key: '', label: chunter.string.ChannelName },
|
||||
{ key: 'attachedToClass', label: view.string.Type },
|
||||
'modifiedOn'
|
||||
],
|
||||
baseMenuClass: notification.class.DocNotifyContext,
|
||||
query: {
|
||||
_class: notification.class.DocNotifyContext,
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
visibleIf: notification.function.HasHiddenDocNotifyContext
|
||||
},
|
||||
{
|
||||
id: 'chunterBrowser',
|
||||
label: chunter.string.ChunterBrowser,
|
||||
icon: workbench.icon.Search,
|
||||
component: chunter.component.ChunterBrowser,
|
||||
visibleIf: chunter.function.ChunterBrowserVisible
|
||||
}
|
||||
]
|
||||
|
||||
export const chatNavGroups: ChatNavGroupModel[] = [
|
||||
{
|
||||
id: 'pinned',
|
||||
label: notification.string.Pinned,
|
||||
hideEmpty: true,
|
||||
query: {
|
||||
isPinned: true
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
icon: view.icon.Delete,
|
||||
label: view.string.Delete,
|
||||
action: chunter.actionImpl.UnpinAllChannels
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'documents',
|
||||
label: chunter.string.Docs,
|
||||
query: {
|
||||
attachedToClass: {
|
||||
$nin: [
|
||||
chunter.class.DirectMessage,
|
||||
chunter.class.Channel,
|
||||
activity.class.DocUpdateMessage,
|
||||
chunter.class.ChatMessage,
|
||||
chunter.class.ThreadMessage,
|
||||
chunter.class.Backlink
|
||||
]
|
||||
},
|
||||
isPinned: { $ne: true }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'channels',
|
||||
label: chunter.string.Channels,
|
||||
query: {
|
||||
attachedToClass: { $in: [chunter.class.Channel] },
|
||||
isPinned: { $ne: true }
|
||||
},
|
||||
addLabel: chunter.string.CreateChannel,
|
||||
addComponent: chunter.component.CreateChannel
|
||||
},
|
||||
{
|
||||
id: 'direct',
|
||||
label: chunter.string.DirectMessages,
|
||||
query: {
|
||||
attachedToClass: { $in: [chunter.class.DirectMessage] },
|
||||
isPinned: { $ne: true }
|
||||
},
|
||||
addLabel: chunter.string.NewDirectMessage,
|
||||
addComponent: chunter.component.CreateDirectMessage
|
||||
}
|
||||
]
|
||||
|
||||
export async function getDocByNotifyContext (
|
||||
docNotifyContexts: DocNotifyContext[]
|
||||
): Promise<Map<Ref<DocNotifyContext>, Doc>> {
|
||||
const client = getClient()
|
||||
|
||||
const docs = await Promise.all(
|
||||
docNotifyContexts.map(
|
||||
async ({ attachedTo, attachedToClass }) => await client.findOne(attachedToClass, { _id: attachedTo })
|
||||
)
|
||||
)
|
||||
|
||||
const result: Map<Ref<DocNotifyContext>, Doc> = new Map<Ref<DocNotifyContext>, Doc>()
|
||||
|
||||
for (const doc of docs) {
|
||||
if (doc === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
const context = docNotifyContexts.find(({ attachedTo }) => attachedTo === doc._id)
|
||||
|
||||
if (context === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
result.set(context._id, doc)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user