UBERF-4360: rewrite chat (#4265)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-01-11 18:46:11 +04:00 committed by GitHub
parent 06f6f6a222
commit 7f6f5e28a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
183 changed files with 6360 additions and 5051 deletions

View File

@ -27,11 +27,15 @@
"dependencies": { "dependencies": {
"@hcengineering/activity": "^0.6.0", "@hcengineering/activity": "^0.6.0",
"@hcengineering/activity-resources": "^0.6.1", "@hcengineering/activity-resources": "^0.6.1",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28", "@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7", "@hcengineering/model": "^0.6.7",
"@hcengineering/model-core": "^0.6.0", "@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/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.11", "@hcengineering/ui": "^0.6.11",
"@hcengineering/model-view": "^0.6.0" "@hcengineering/view": "^0.6.9"
} }
} }

View File

@ -31,6 +31,7 @@ import {
type Reaction, type Reaction,
type TxViewlet, type TxViewlet,
type ActivityMessageControl, type ActivityMessageControl,
type SavedMessage,
type IgnoreActivity type IgnoreActivity
} from '@hcengineering/activity' } from '@hcengineering/activity'
import core, { import core, {
@ -43,7 +44,8 @@ import core, {
IndexKind, IndexKind,
type TxCUD, type TxCUD,
type Domain, type Domain,
type Account type Account,
type Timestamp
} from '@hcengineering/core' } from '@hcengineering/core'
import { import {
Model, Model,
@ -55,11 +57,16 @@ import {
Mixin, Mixin,
Collection, Collection,
TypeBoolean, TypeBoolean,
TypeIntlString TypeIntlString,
ArrOf,
TypeTimestamp
} from '@hcengineering/model' } from '@hcengineering/model'
import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core' import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import type { Asset, IntlString, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types' 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 view from '@hcengineering/model-view'
import activity from './plugin' import activity from './plugin'
@ -102,8 +109,18 @@ export class TActivityMessage extends TAttachedDoc implements ActivityMessage {
@Prop(TypeBoolean(), activity.string.Pinned) @Prop(TypeBoolean(), activity.string.Pinned)
isPinned?: boolean 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) @Prop(Collection(activity.class.Reaction), activity.string.Reactions)
reactions?: number reactions?: number
@Prop(Collection(activity.class.ActivityMessage), activity.string.Replies)
replies?: number
} }
@Model(activity.class.DocUpdateMessage, activity.class.ActivityMessage) @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) @Model(activity.class.ActivityMessagesFilter, core.class.Doc, DOMAIN_MODEL)
export class TActivityMessagesFilter extends TDoc implements ActivityMessagesFilter { export class TActivityMessagesFilter extends TDoc implements ActivityMessagesFilter {
label!: IntlString label!: IntlString
position!: number
filter!: Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean> filter!: Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
} }
@Model(activity.class.Reaction, core.class.AttachedDoc, DOMAIN_ACTIVITY) @Model(activity.class.Reaction, core.class.AttachedDoc, DOMAIN_ACTIVITY)
export class TReaction extends TAttachedDoc implements Reaction { 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) @Prop(TypeString(), activity.string.Emoji)
emoji!: string emoji!: string
@ -198,6 +224,11 @@ export class TReaction extends TAttachedDoc implements Reaction {
createBy!: Ref<Account> 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 { export function createModel (builder: Builder): void {
builder.createModel( builder.createModel(
TTxViewlet, TTxViewlet,
@ -212,6 +243,7 @@ export function createModel (builder: Builder): void {
TActivityAttributeUpdatesPresenter, TActivityAttributeUpdatesPresenter,
TActivityInfoMessage, TActivityInfoMessage,
TActivityMessageControl, TActivityMessageControl,
TSavedMessage,
TIgnoreActivity TIgnoreActivity
) )
@ -225,13 +257,30 @@ export function createModel (builder: Builder): void {
presenter: activity.component.ActivityInfoMessagePresenter 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, { builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
label: activity.string.Attributes, label: activity.string.Attributes,
position: 10,
filter: activity.filter.AttributesFilter filter: activity.filter.AttributesFilter
}) })
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, { builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
label: activity.string.Pinned, label: activity.string.Pinned,
position: 20,
filter: activity.filter.PinnedFilter filter: activity.filter.PinnedFilter
}) })
@ -259,6 +308,31 @@ export function createModel (builder: Builder): void {
}, },
activity.ids.ReactionRemovedActivityViewlet 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 export default activity

View File

@ -16,20 +16,33 @@ import { activityId, type ActivityMessage, type DocUpdateMessageViewlet } from '
import activity from '@hcengineering/activity-resources/src/plugin' import activity from '@hcengineering/activity-resources/src/plugin'
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform' import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
import { type Doc, type Ref } from '@hcengineering/core' 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, { export default mergeIds(activityId, activity, {
string: { string: {
Attributes: '' as IntlString, Attributes: '' as IntlString,
Pinned: '' as IntlString, Pinned: '' as IntlString,
Emoji: '' as IntlString, Emoji: '' as IntlString,
Reacted: '' as IntlString Reacted: '' as IntlString,
Replies: '' as IntlString
}, },
filter: { filter: {
AttributesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>, 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: { ids: {
ReactionAddedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>, 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>
} }
}) })

View File

@ -55,12 +55,13 @@ export const migrateOperations: [string, MigrateOperation][] = [
['view', viewOperation], ['view', viewOperation],
['contact', contactOperation], ['contact', contactOperation],
['tags', tagsOperation], ['tags', tagsOperation],
['notification', notificationOperation],
['setting', settingOperation], ['setting', settingOperation],
['tracker', trackerOperation], ['tracker', trackerOperation],
['board', boardOperation], ['board', boardOperation],
['hr', hrOperation], ['hr', hrOperation],
['bitrix', bitrixOperation], ['bitrix', bitrixOperation],
['inventiry', inventoryOperation], ['inventiry', inventoryOperation],
['activityServer', activityServerOperation] ['activityServer', activityServerOperation],
// We should call it after activityServer and chunter
['notification', notificationOperation]
] ]

View File

@ -31,6 +31,7 @@ import {
import core, { TAttachedDoc } from '@hcengineering/model-core' import core, { TAttachedDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference' import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction } from '@hcengineering/model-view' import view, { createAction } from '@hcengineering/model-view'
import attachment from './plugin' import attachment from './plugin'
export { attachmentId } from '@hcengineering/attachment' export { attachmentId } from '@hcengineering/attachment'
@ -150,6 +151,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, { builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
label: attachment.string.FilterAttachments, label: attachment.string.FilterAttachments,
position: 50,
filter: attachment.filter.AttachmentsFilter filter: attachment.filter.AttachmentsFilter
}) })

View File

@ -24,8 +24,7 @@ import type { ActionCategory } from '@hcengineering/view'
export default mergeIds(attachmentId, attachment, { export default mergeIds(attachmentId, attachment, {
component: { component: {
AttachmentPresenter: '' as AnyComponent, AttachmentPresenter: '' as AnyComponent
FileBrowser: '' as AnyComponent
}, },
string: { string: {
AddAttachment: '' as IntlString, AddAttachment: '' as IntlString,

View File

@ -36,7 +36,6 @@
"@hcengineering/model-activity": "^0.6.0", "@hcengineering/model-activity": "^0.6.0",
"@hcengineering/model-core": "^0.6.0", "@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-notification": "^0.6.0", "@hcengineering/model-notification": "^0.6.0",
"@hcengineering/model-preference": "^0.6.0",
"@hcengineering/model-view": "^0.6.0", "@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1", "@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/notification": "^0.6.16", "@hcengineering/notification": "^0.6.16",

View File

@ -13,22 +13,22 @@
// limitations under the License. // limitations under the License.
// //
import activity from '@hcengineering/activity' import activity, { type ActivityMessage } from '@hcengineering/activity'
import { import {
type Backlink, type Backlink,
type Channel, type Channel,
chunterId, chunterId,
type ChunterMessage, type ChunterMessage,
type ChunterMessageExtension, type ChunterMessageExtension,
type ChunterSpace,
type Comment, type Comment,
type DirectMessage, type DirectMessage,
type Message, type Message,
type SavedMessages,
type ThreadMessage,
type DirectMessageInput, type DirectMessageInput,
type ChatMessage, type ChatMessage,
type ChatMessageViewlet type ChatMessageViewlet,
type ChunterSpace,
type ObjectChatPanel,
type ThreadMessage
} from '@hcengineering/chunter' } from '@hcengineering/chunter'
import contact, { type Person } from '@hcengineering/contact' import contact, { type Person } from '@hcengineering/contact'
import { import {
@ -61,14 +61,14 @@ import {
import attachment from '@hcengineering/model-attachment' import attachment from '@hcengineering/model-attachment'
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core' import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
import notification from '@hcengineering/model-notification' import notification from '@hcengineering/model-notification'
import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view' import view, { createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench' import workbench from '@hcengineering/model-workbench'
import chunter from './plugin' import chunter from './plugin'
import { type AnyComponent } from '@hcengineering/ui/src/types' import { type AnyComponent } from '@hcengineering/ui/src/types'
import { TypeBoolean } from '@hcengineering/model' import { TypeBoolean } from '@hcengineering/model'
import type { IntlString } from '@hcengineering/platform' import type { IntlString, Resource } from '@hcengineering/platform'
import { TActivityMessage } from '@hcengineering/model-activity' import { TActivityMessage } from '@hcengineering/model-activity'
export { chunterId } from '@hcengineering/chunter' export { chunterId } from '@hcengineering/chunter'
export { chunterOperation } from './migration' export { chunterOperation } from './migration'
@ -119,14 +119,6 @@ export class TChunterMessage extends TAttachedDoc implements ChunterMessage {
@Mixin(chunter.mixin.ChunterMessageExtension, chunter.class.ChunterMessage) @Mixin(chunter.mixin.ChunterMessageExtension, chunter.class.ChunterMessage)
export class TChunterMessageExtension extends TChunterMessage implements ChunterMessageExtension {} 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) @Model(chunter.class.Message, chunter.class.ChunterMessage)
@UX(chunter.string.Message, undefined, 'MSG') @UX(chunter.string.Message, undefined, 'MSG')
export class TMessage extends TChunterMessage implements Message { export class TMessage extends TChunterMessage implements Message {
@ -139,7 +131,7 @@ export class TMessage extends TChunterMessage implements Message {
repliesCount?: number repliesCount?: number
@Prop(TypeTimestamp(), chunter.string.LastReply) @Prop(TypeTimestamp(), activity.string.LastReply)
lastReply?: Timestamp lastReply?: Timestamp
} }
@ -167,12 +159,6 @@ export class TBacklink extends TComment implements Backlink {
backlinkClass!: Ref<Class<Doc>> 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) @Mixin(chunter.mixin.DirectMessageInput, core.class.Class)
export class TDirectMessageInput extends TClass implements DirectMessageInput { export class TDirectMessageInput extends TClass implements DirectMessageInput {
component!: AnyComponent component!: AnyComponent
@ -184,13 +170,31 @@ export class TChatMessage extends TActivityMessage implements ChatMessage {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
message!: string message!: string
@Prop(TypeTimestamp(), chunter.string.Edit)
@Index(IndexKind.Indexed)
editedOn?: Timestamp
@Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, { @Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, {
shortLabel: attachment.string.Files shortLabel: attachment.string.Files
}) })
attachments?: number attachments?: number
}
@Prop(TypeBoolean(), core.string.Boolean) @Model(chunter.class.ThreadMessage, chunter.class.ChatMessage)
isEdited?: boolean 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) @Model(chunter.class.ChatMessageViewlet, core.class.Doc, DOMAIN_MODEL)
@ -199,31 +203,45 @@ export class TChatMessageViewlet extends TDoc implements ChatMessageViewlet {
@Index(IndexKind.Indexed) @Index(IndexKind.Indexed)
objectClass!: Ref<Class<Doc>> objectClass!: Ref<Class<Doc>>
@Prop(TypeRef(core.class.Doc), core.string.Class)
@Index(IndexKind.Indexed)
messageClass!: Ref<Class<Doc>>
label?: IntlString label?: IntlString
onlyWithParent?: boolean 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 { export function createModel (builder: Builder, options = { addApplication: true }): void {
builder.createModel( builder.createModel(
TChunterSpace, TChunterSpace,
TChannel, TChannel,
TMessage, TMessage,
TThreadMessage,
TChunterMessage, TChunterMessage,
TChunterMessageExtension, TChunterMessageExtension,
TComment, TComment,
TBacklink, TBacklink,
TDirectMessage, TDirectMessage,
TSavedMessages,
TDirectMessageInput, TDirectMessageInput,
TChatMessage, TChatMessage,
TChatMessageViewlet TThreadMessage,
TChatMessageViewlet,
TObjectChatPanel
) )
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage] const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
spaceClasses.forEach((spaceClass) => { spaceClasses.forEach((spaceClass) => {
builder.mixin(spaceClass, core.class.Class, activity.mixin.ActivityDoc, {}) 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, { builder.mixin(spaceClass, core.class.Class, workbench.mixin.SpaceView, {
view: { view: {
class: chunter.class.Message 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, { builder.mixin(chunter.class.Message, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['createdBy', 'replies'] 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, { builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['members'] fields: ['members']
}) })
@ -279,10 +297,6 @@ export function createModel (builder: Builder, options = { addApplication: true
presenter: chunter.component.ThreadParentPresenter 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, { builder.mixin(chunter.class.Message, core.class.Class, view.mixin.ObjectPanel, {
component: chunter.component.ThreadViewPanel component: chunter.component.ThreadViewPanel
}) })
@ -303,23 +317,6 @@ export function createModel (builder: Builder, options = { addApplication: true
header: chunter.component.ChannelHeader 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( builder.createDoc(
view.class.ActionCategory, view.class.ActionCategory,
core.space.Model, core.space.Model,
@ -327,38 +324,6 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.category.Chunter 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( createAction(
builder, builder,
{ {
@ -423,164 +388,20 @@ export function createModel (builder: Builder, options = { addApplication: true
{ {
label: chunter.string.ApplicationLabelChunter, label: chunter.string.ApplicationLabelChunter,
icon: chunter.icon.Chunter, icon: chunter.icon.Chunter,
locationResolver: chunter.resolver.Location,
alias: chunterId, alias: chunterId,
hidden: false, hidden: false,
navigatorModel: { component: chunter.component.Chat,
specials: [ aside: chunter.component.ThreadView,
{ shouldNotify: chunter.function.ShouldNotify
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
}
}, },
chunter.app.Chunter 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, { builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.LinkProvider, {
encode: chunter.function.GetFragment encode: chunter.function.GetFragment
}) })
builder.mixin(chunter.class.Message, core.class.Class, view.mixin.LinkProvider, {
encode: chunter.function.GetFragment
})
createAction( createAction(
builder, builder,
{ {
@ -593,114 +414,19 @@ export function createModel (builder: Builder, options = { addApplication: true
keyBinding: [], keyBinding: [],
input: 'none', input: 'none',
category: chunter.category.Chunter, category: chunter.category.Chunter,
target: chunter.class.Message, target: activity.class.ActivityMessage,
context: { context: {
mode: ['context', 'browser'], mode: ['context', 'browser'],
application: chunter.app.Chunter, application: chunter.app.Chunter,
group: 'copy' group: 'copy'
} }
}, },
chunter.action.CopyMessageLink chunter.action.CopyChatMessageLink
)
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
) )
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, { builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
label: chunter.string.FilterBacklinks, label: chunter.string.FilterBacklinks,
position: 60,
filter: chunter.filter.BacklinksFilter filter: chunter.filter.BacklinksFilter
}) })
@ -753,7 +479,7 @@ export function createModel (builder: Builder, options = { addApplication: true
generated: false, generated: false,
hidden: false, hidden: false,
txClasses: [core.class.TxCreateDoc], txClasses: [core.class.TxCreateDoc],
objectClass: chunter.class.Message, objectClass: chunter.class.ChatMessage,
providers: { providers: {
[notification.providers.EmailNotification]: false, [notification.providers.EmailNotification]: false,
[notification.providers.PlatformNotification]: true [notification.providers.PlatformNotification]: true
@ -776,7 +502,7 @@ export function createModel (builder: Builder, options = { addApplication: true
generated: false, generated: false,
hidden: false, hidden: false,
txClasses: [core.class.TxCreateDoc], txClasses: [core.class.TxCreateDoc],
objectClass: chunter.class.Message, objectClass: chunter.class.ChatMessage,
providers: { providers: {
[notification.providers.PlatformNotification]: true [notification.providers.PlatformNotification]: true
}, },
@ -841,10 +567,16 @@ export function createModel (builder: Builder, options = { addApplication: true
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, { builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
label: chunter.string.Comments, label: chunter.string.Comments,
position: 60,
filter: chunter.filter.ChatMessagesFilter filter: chunter.filter.ChatMessagesFilter
}) })
builder.mixin(chunter.class.ChatMessage, core.class.Class, activity.mixin.ActivityDoc, {}) 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, { builder.mixin(chunter.class.ChatMessage, core.class.Class, view.mixin.CollectionPresenter, {
presenter: chunter.component.ChatMessagesPresenter presenter: chunter.component.ChatMessagesPresenter
}) })
@ -853,6 +585,21 @@ export function createModel (builder: Builder, options = { addApplication: true
presenter: chunter.component.ChatMessagePresenter 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( createAction(
builder, builder,
{ {
@ -863,10 +610,59 @@ export function createModel (builder: Builder, options = { addApplication: true
keyBinding: ['Backspace'], keyBinding: ['Backspace'],
category: chunter.category.Chunter, category: chunter.category.Chunter,
target: chunter.class.ChatMessage, target: chunter.class.ChatMessage,
context: { mode: ['context', 'browser'], group: 'edit' } visibilityTester: chunter.function.CanDeleteMessage,
context: { mode: ['context', 'browser'], group: 'remove' }
}, },
chunter.action.DeleteChatMessage 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 export default chunter

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import core, { TxOperations } from '@hcengineering/core' import core, { type Class, type Doc, type Ref, TxOperations } from '@hcengineering/core'
import { import {
type MigrateOperation, type MigrateOperation,
type MigrationClient, type MigrationClient,
@ -22,14 +22,44 @@ import {
} from '@hcengineering/model' } from '@hcengineering/model'
import { chunterId } from '@hcengineering/chunter' import { chunterId } from '@hcengineering/chunter'
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity' import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import notification from '@hcengineering/notification'
import { DOMAIN_COMMENT } from './index' import { DOMAIN_COMMENT } from './index'
import chunter from './plugin' 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, { const createTx = await tx.findOne(core.class.TxCreateDoc, {
objectId: chunter.space.General objectId: chunter.space.General
}) })
if (createTx === undefined) { if (createTx === undefined) {
await tx.createDoc( await tx.createDoc(
chunter.class.Channel, chunter.class.Channel,
@ -45,12 +75,15 @@ export async function createGeneral (tx: TxOperations): Promise<void> {
chunter.space.General 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, { const createTx = await tx.findOne(core.class.TxCreateDoc, {
objectId: chunter.space.Random objectId: chunter.space.Random
}) })
if (createTx === undefined) { if (createTx === undefined) {
await tx.createDoc( await tx.createDoc(
chunter.class.Channel, chunter.class.Channel,
@ -66,6 +99,8 @@ export async function createRandom (tx: TxOperations): Promise<void> {
chunter.space.Random chunter.space.Random
) )
} }
await createDocNotifyContexts(client, tx, chunter.space.Random, chunter.class.Channel)
} }
async function createBacklink (tx: TxOperations): Promise<void> { async function createBacklink (tx: TxOperations): Promise<void> {
@ -104,8 +139,8 @@ export const chunterOperation: MigrateOperation = {
}, },
async upgrade (client: MigrationUpgradeClient): Promise<void> { async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System) const tx = new TxOperations(client, core.account.System)
await createGeneral(tx) await createGeneral(client, tx)
await createRandom(tx) await createRandom(client, tx)
await createBacklink(tx) await createBacklink(tx)
} }
} }

View File

@ -16,8 +16,8 @@
import type { ActivityMessage, DocUpdateMessageViewlet, TxViewlet } from '@hcengineering/activity' import type { ActivityMessage, DocUpdateMessageViewlet, TxViewlet } from '@hcengineering/activity'
import { chunterId, type Channel } from '@hcengineering/chunter' import { chunterId, type Channel } from '@hcengineering/chunter'
import chunter from '@hcengineering/chunter-resources/src/plugin' import chunter from '@hcengineering/chunter-resources/src/plugin'
import type { Doc, Ref, Space } from '@hcengineering/core' import { type Client, type Doc, type Ref } from '@hcengineering/core'
import { type NotificationGroup } from '@hcengineering/notification' import { type DocNotifyContext, type NotificationGroup } from '@hcengineering/notification'
import type { IntlString, Resource } from '@hcengineering/platform' import type { IntlString, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform'
import type { AnyComponent, Location } from '@hcengineering/ui/src/types' import type { AnyComponent, Location } from '@hcengineering/ui/src/types'
@ -29,11 +29,10 @@ export default mergeIds(chunterId, chunter, {
DirectMessagePresenter: '' as AnyComponent, DirectMessagePresenter: '' as AnyComponent,
MessagePresenter: '' as AnyComponent, MessagePresenter: '' as AnyComponent,
DmPresenter: '' as AnyComponent, DmPresenter: '' as AnyComponent,
Threads: '' as AnyComponent,
SavedMessages: '' as AnyComponent,
ChunterBrowser: '' as AnyComponent,
BacklinkContent: '' as AnyComponent, BacklinkContent: '' as AnyComponent,
BacklinkReference: '' as AnyComponent BacklinkReference: '' as AnyComponent,
ChannelsPanel: '' as AnyComponent,
Chat: '' as AnyComponent
}, },
action: { action: {
MarkCommentUnread: '' as Ref<Action>, MarkCommentUnread: '' as Ref<Action>,
@ -41,17 +40,15 @@ export default mergeIds(chunterId, chunter, {
ArchiveChannel: '' as Ref<Action>, ArchiveChannel: '' as Ref<Action>,
UnarchiveChannel: '' as Ref<Action>, UnarchiveChannel: '' as Ref<Action>,
ConvertToPrivate: '' as Ref<Action>, ConvertToPrivate: '' as Ref<Action>,
CopyCommentLink: '' as Ref<Action<Doc, any>>, CopyChatMessageLink: '' as Ref<Action<Doc, any>>,
CopyThreadMessageLink: '' as Ref<Action<Doc, any>>, OpenChannel: '' as Ref<Action>
CopyMessageLink: '' as Ref<Action<Doc, any>>
}, },
actionImpl: { actionImpl: {
MarkUnread: '' as ViewAction,
MarkCommentUnread: '' as ViewAction,
ArchiveChannel: '' as ViewAction, ArchiveChannel: '' as ViewAction,
UnarchiveChannel: '' as ViewAction, UnarchiveChannel: '' as ViewAction,
ConvertDmToPrivateChannel: '' as ViewAction, ConvertDmToPrivateChannel: '' as ViewAction,
DeleteChatMessage: '' as ViewAction DeleteChatMessage: '' as ViewAction,
ReplyToThread: '' as ViewAction
}, },
category: { category: {
Chunter: '' as Ref<ActionCategory> Chunter: '' as Ref<ActionCategory>
@ -62,7 +59,6 @@ export default mergeIds(chunterId, chunter, {
Content: '' as IntlString, Content: '' as IntlString,
Comment: '' as IntlString, Comment: '' as IntlString,
Reference: '' as IntlString, Reference: '' as IntlString,
Chat: '' as IntlString,
CreateBy: '' as IntlString, CreateBy: '' as IntlString,
Create: '' as IntlString, Create: '' as IntlString,
Edit: '' as IntlString, Edit: '' as IntlString,
@ -71,14 +67,15 @@ export default mergeIds(chunterId, chunter, {
MentionNotification: '' as IntlString, MentionNotification: '' as IntlString,
PinnedMessages: '' as IntlString, PinnedMessages: '' as IntlString,
SavedMessages: '' as IntlString, SavedMessages: '' as IntlString,
ThreadMessage: '' as IntlString,
Emoji: '' as IntlString, Emoji: '' as IntlString,
FilterBacklinks: '' as IntlString, FilterBacklinks: '' as IntlString,
DM: '' as IntlString, DM: '' as IntlString,
DMNotification: '' as IntlString, DMNotification: '' as IntlString,
ConfigLabel: '' as IntlString, ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString, ConfigDescription: '' as IntlString,
Reacted: '' as IntlString Reacted: '' as IntlString,
Saved: '' as IntlString,
RepliedToThread: '' as IntlString
}, },
viewlet: { viewlet: {
Chat: '' as Ref<ViewletDescriptor> Chat: '' as Ref<ViewletDescriptor>
@ -105,9 +102,12 @@ export default mergeIds(chunterId, chunter, {
Random: '' as Ref<Channel> Random: '' as Ref<Channel>
}, },
function: { function: {
ChunterBrowserVisible: '' as Resource<(spaces: Space[]) => Promise<boolean>>,
GetLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>, 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: { filter: {
BacklinksFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>, BacklinksFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,

View File

@ -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, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: contact.class.Contact, ofClass: contact.class.Contact,
components: { input: chunter.component.ChatMessageInput } components: { input: chunter.component.ChatMessageInput }
@ -280,6 +282,7 @@ export function createModel (builder: Builder): void {
hidden: false, hidden: false,
// component: contact.component.ContactsTabs, // component: contact.component.ContactsTabs,
locationResolver: contact.resolver.Location, locationResolver: contact.resolver.Location,
aside: chunter.component.ThreadView,
navigatorModel: { navigatorModel: {
spaces: [], spaces: [],
specials: [ specials: [
@ -988,6 +991,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: contact.class.Person, objectClass: contact.class.Person,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },
@ -998,6 +1002,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: contact.mixin.Employee, objectClass: contact.mixin.Employee,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },
@ -1008,6 +1013,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: contact.class.Organization, objectClass: contact.class.Organization,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },

View File

@ -148,6 +148,7 @@ export function createModel (builder: Builder): void {
icon: inventory.icon.InventoryApplication, icon: inventory.icon.InventoryApplication,
alias: inventoryId, alias: inventoryId,
hidden: false, hidden: false,
aside: chunter.component.ThreadView,
navigatorModel: { navigatorModel: {
specials: [ specials: [
{ {
@ -187,6 +188,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: inventory.class.Product, objectClass: inventory.class.Product,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },
@ -197,6 +199,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: inventory.class.Category, objectClass: inventory.class.Category,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },

View File

@ -155,6 +155,7 @@ export function createModel (builder: Builder): void {
icon: lead.icon.LeadApplication, icon: lead.icon.LeadApplication,
alias: leadId, alias: leadId,
hidden: false, hidden: false,
aside: chunter.component.ThreadView,
navigatorModel: { navigatorModel: {
specials: [ specials: [
{ {
@ -542,6 +543,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: lead.class.Lead, objectClass: lead.class.Lead,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },

View File

@ -43,7 +43,8 @@ import {
TypeString, TypeString,
UX, UX,
TypeBoolean, TypeBoolean,
TypeDate TypeDate,
TypeIntlString
} from '@hcengineering/model' } from '@hcengineering/model'
import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core' import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference' import preference, { TPreference } from '@hcengineering/model-preference'
@ -65,7 +66,9 @@ import {
type NotificationTemplate, type NotificationTemplate,
type NotificationType, type NotificationType,
notificationId, notificationId,
type NotificationObjectPresenter type NotificationObjectPresenter,
type ActivityInboxNotification,
type CommonInboxNotification
} from '@hcengineering/notification' } from '@hcengineering/notification'
import { type Asset, type IntlString } from '@hcengineering/platform' import { type Asset, type IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
@ -197,18 +200,13 @@ export class TDocNotifyContext extends TDoc implements DocNotifyContext {
@Prop(TypeDate(), core.string.Date) @Prop(TypeDate(), core.string.Date)
@Index(IndexKind.Indexed) @Index(IndexKind.Indexed)
lastUpdateTimestamp?: Timestamp lastUpdateTimestamp?: Timestamp
@Prop(TypeBoolean(), notification.string.Pinned)
isPinned?: boolean
} }
@Model(notification.class.InboxNotification, core.class.Doc, DOMAIN_NOTIFICATION) @Model(notification.class.InboxNotification, core.class.Doc, DOMAIN_NOTIFICATION)
export class TInboxNotification extends TDoc implements InboxNotification { 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) @Prop(TypeRef(notification.class.DocNotifyContext), core.string.AttachedTo)
@Index(IndexKind.Indexed) @Index(IndexKind.Indexed)
docNotifyContext!: Ref<DocNotifyContext> docNotifyContext!: Ref<DocNotifyContext>
@ -220,6 +218,32 @@ export class TInboxNotification extends TDoc implements InboxNotification {
@Prop(TypeBoolean(), core.string.Boolean) @Prop(TypeBoolean(), core.string.Boolean)
@Index(IndexKind.Indexed) @Index(IndexKind.Indexed)
isViewed!: boolean 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 { export function createModel (builder: Builder): void {
@ -236,7 +260,9 @@ export function createModel (builder: Builder): void {
TNotificationObjectPresenter, TNotificationObjectPresenter,
TNotificationPreview, TNotificationPreview,
TDocNotifyContext, TDocNotifyContext,
TInboxNotification TInboxNotification,
TActivityInboxNotification,
TCommonInboxNotification
) )
// Temporarily disabled, we should think about it // Temporarily disabled, we should think about it
@ -283,20 +309,6 @@ export function createModel (builder: Builder): void {
notification.ids.NotificationSettings 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( builder.createDoc(
workbench.class.Application, workbench.class.Application,
core.space.Model, core.space.Model,
@ -312,7 +324,7 @@ export function createModel (builder: Builder): void {
specials: [ specials: [
{ {
id: 'all', id: 'all',
component: notification.component.NewInbox, component: notification.component.Inbox,
icon: activity.icon.Activity, icon: activity.icon.Activity,
label: activity.string.AllActivity, label: activity.string.AllActivity,
componentProps: { componentProps: {
@ -322,7 +334,7 @@ export function createModel (builder: Builder): void {
}, },
{ {
id: 'reactions', id: 'reactions',
component: notification.component.NewInbox, component: notification.component.Inbox,
icon: activity.icon.Emoji, icon: activity.icon.Emoji,
label: activity.string.Reactions, label: activity.string.Reactions,
componentProps: { componentProps: {
@ -336,57 +348,6 @@ export function createModel (builder: Builder): void {
notification.app.Inbox 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, { createAction(builder, {
action: workbench.actionImpl.Navigate, action: workbench.actionImpl.Navigate,
actionProps: { actionProps: {
@ -486,7 +447,7 @@ export function createModel (builder: Builder): void {
input: 'focus', input: 'focus',
visibilityTester: notification.function.HasMarkAsReadAction, visibilityTester: notification.function.HasMarkAsReadAction,
category: notification.category.Notification, category: notification.category.Notification,
target: activity.class.ActivityMessage, target: notification.class.InboxNotification,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' } context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
}, },
notification.action.MarkAsReadInboxNotification notification.action.MarkAsReadInboxNotification
@ -501,7 +462,7 @@ export function createModel (builder: Builder): void {
input: 'focus', input: 'focus',
visibilityTester: notification.function.HasMarkAsUnreadAction, visibilityTester: notification.function.HasMarkAsUnreadAction,
category: notification.category.Notification, category: notification.category.Notification,
target: activity.class.ActivityMessage, target: notification.class.InboxNotification,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' } context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
}, },
notification.action.MarkAsUnreadInboxNotification notification.action.MarkAsUnreadInboxNotification
@ -516,12 +477,89 @@ export function createModel (builder: Builder): void {
input: 'focus', input: 'focus',
keyBinding: ['Backspace'], keyBinding: ['Backspace'],
category: notification.category.Notification, category: notification.category.Notification,
visibilityTester: notification.function.HasDeleteNotificationAction, target: notification.class.InboxNotification,
target: activity.class.ActivityMessage,
context: { mode: ['context', 'browser'], group: 'edit' } context: { mode: ['context', 'browser'], group: 'edit' }
}, },
notification.action.DeleteInboxNotification 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 ( export function generateClassNotificationTypes (

View File

@ -13,9 +13,41 @@
// limitations under the License. // limitations under the License.
// //
import core, { TxOperations } from '@hcengineering/core' import core, {
import { type MigrateOperation, type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model' type AttachedDoc,
import notification from '@hcengineering/notification' 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> { async function createSpace (client: MigrationUpgradeClient): Promise<void> {
const txop = new TxOperations(client, core.account.System) 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 = { 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> { async upgrade (client: MigrationUpgradeClient): Promise<void> {
await createSpace(client) await createSpace(client)
} }

View File

@ -31,7 +31,8 @@ export default mergeIds(notificationId, notification, {
Archive: '' as IntlString, Archive: '' as IntlString,
MarkAsUnread: '' as IntlString, MarkAsUnread: '' as IntlString,
MarkAsRead: '' as IntlString, MarkAsRead: '' as IntlString,
ChangeCollaborators: '' as IntlString ChangeCollaborators: '' as IntlString,
Message: '' as IntlString
}, },
app: { app: {
Notification: '' as Ref<Application>, Notification: '' as Ref<Application>,
@ -48,31 +49,30 @@ export default mergeIds(notificationId, notification, {
component: { component: {
NotificationSettings: '' as AnyComponent, NotificationSettings: '' as AnyComponent,
InboxAside: '' as AnyComponent, InboxAside: '' as AnyComponent,
ChatMessagePresenter: '' as AnyComponent, ActivityInboxNotificationPresenter: '' as AnyComponent,
DocUpdateMessagePresenter: '' as AnyComponent, CommonInboxNotificationPresenter: '' as AnyComponent
PinMessageAction: '' as AnyComponent
}, },
function: { function: {
HasntNotifications: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasMarkAsUnreadAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>, HasMarkAsUnreadAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasMarkAsReadAction: '' 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: { category: {
Notification: '' as Ref<ActionCategory> Notification: '' as Ref<ActionCategory>
}, },
groups: {}, groups: {},
action: { action: {
Unsubscribe: '' as Ref<Action>, Unsubscribe: '' as Ref<Action>
Hide: '' as Ref<Action>,
MarkAsUnread: '' as Ref<Action>
}, },
actionImpl: { actionImpl: {
Unsubscribe: '' as ViewAction, Unsubscribe: '' as ViewAction,
Hide: '' as ViewAction,
MarkAsUnread: '' as ViewAction,
MarkAsUnreadInboxNotification: '' as ViewAction, MarkAsUnreadInboxNotification: '' as ViewAction,
MarkAsReadInboxNotification: '' as ViewAction, MarkAsReadInboxNotification: '' as ViewAction,
DeleteInboxNotification: '' as ViewAction DeleteInboxNotification: '' as ViewAction,
UnpinDocNotifyContext: '' as ViewAction,
PinDocNotifyContext: '' as ViewAction,
HideDocNotifyContext: '' as ViewAction,
UnHideDocNotifyContext: '' as ViewAction
} }
}) })

View File

@ -298,6 +298,7 @@ export function createModel (builder: Builder): void {
locationResolver: recruit.resolver.Location, locationResolver: recruit.resolver.Location,
alias: recruitId, alias: recruitId,
hidden: false, hidden: false,
aside: chunter.component.ThreadView,
navigatorModel: { navigatorModel: {
spaces: [], spaces: [],
specials: [ specials: [
@ -1619,6 +1620,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: recruit.class.Vacancy, objectClass: recruit.class.Vacancy,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },
@ -1629,6 +1631,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: recruit.class.Applicant, objectClass: recruit.class.Applicant,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },
@ -1639,6 +1642,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: recruit.class.Review, objectClass: recruit.class.Review,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },

View File

@ -28,6 +28,7 @@
"@hcengineering/activity": "^0.6.0", "@hcengineering/activity": "^0.6.0",
"@hcengineering/core": "^0.6.28", "@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7", "@hcengineering/model": "^0.6.7",
"@hcengineering/model-activity": "^0.6.0",
"@hcengineering/platform": "^0.6.9", "@hcengineering/platform": "^0.6.9",
"@hcengineering/server-activity": "^0.6.0", "@hcengineering/server-activity": "^0.6.0",
"@hcengineering/server-activity-resources": "^0.6.0", "@hcengineering/server-activity-resources": "^0.6.0",

View File

@ -22,15 +22,13 @@ export { activityServerOperation } from './migration'
export { serverActivityId } from '@hcengineering/server-activity' export { serverActivityId } from '@hcengineering/server-activity'
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
// NOTE: temporarily disabled builder.createDoc(serverCore.class.Trigger, core.space.Model, {
// builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverActivity.trigger.OnReactionChanged,
// trigger: serverNotification.trigger.OnReactionChanged, txMatch: {
// txMatch: { collection: 'reactions',
// collection: 'reactions', _class: core.class.TxCollectionCUD
// objectClass: activity.class.ActivityMessage, }
// _class: core.class.TxCollectionCUD })
// }
// })
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverActivity.trigger.ActivityMessagesHandler trigger: serverActivity.trigger.ActivityMessagesHandler

View File

@ -36,6 +36,7 @@ import {
serverActivityId serverActivityId
} from '@hcengineering/server-activity' } from '@hcengineering/server-activity'
import { generateDocUpdateMessages } from '@hcengineering/server-activity-resources' import { generateDocUpdateMessages } from '@hcengineering/server-activity-resources'
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
function getActivityControl (client: MigrationClient): ActivityControl { function getActivityControl (client: MigrationClient): ActivityControl {
const txFactory = new TxFactory(core.account.System, false) const txFactory = new TxFactory(core.account.System, false)
@ -55,6 +56,16 @@ async function generateDocUpdateMessageByTx (
client: MigrationClient, client: MigrationClient,
objectCache?: DocObjectCache objectCache?: DocObjectCache
): Promise<void> { ): 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) const createCollectionCUDTxes = await generateDocUpdateMessages(tx, control, undefined, undefined, objectCache)
for (const collectionTx of createCollectionCUDTxes) { for (const collectionTx of createCollectionCUDTxes) {
@ -206,8 +217,14 @@ export const activityServerOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, serverActivityId, [ await tryMigrate(client, serverActivityId, [
{ {
state: 'activity-messages', state: 'doc-update-messages',
func: createDocUpdateMessages func: async (client) => {
// Recreate activity to avoid duplicates
await client.deleteMany(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage
})
await createDocUpdateMessages(client)
}
} }
]) ])
}, },

View File

@ -21,6 +21,7 @@ import serverNotification from '@hcengineering/server-notification'
import serverCore, { type ObjectDDParticipant } from '@hcengineering/server-core' import serverCore, { type ObjectDDParticipant } from '@hcengineering/server-core'
import serverChunter from '@hcengineering/server-chunter' import serverChunter from '@hcengineering/server-chunter'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
export { serverChunterId } from '@hcengineering/server-chunter' export { serverChunterId } from '@hcengineering/server-chunter'
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
@ -45,7 +46,7 @@ export function createModel (builder: Builder): void {
presenter: serverChunter.function.ChunterNotificationContentProvider 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 presenter: serverChunter.function.ChunterNotificationContentProvider
}) })
@ -58,11 +59,22 @@ export function createModel (builder: Builder): void {
}) })
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.OnMessageSent, trigger: serverChunter.trigger.OnDirectMessageSent,
txMatch: { txMatch: {
objectClass: chunter.class.DirectMessage,
_class: core.class.TxCollectionCUD, _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
} }
}) })

View File

@ -31,6 +31,7 @@ import serverNotification, {
type NotificationContentProvider type NotificationContentProvider
} from '@hcengineering/server-notification' } from '@hcengineering/server-notification'
import chunter from '@hcengineering/model-chunter' import chunter from '@hcengineering/model-chunter'
import activity from '@hcengineering/activity'
export { serverNotificationId } from '@hcengineering/server-notification' export { serverNotificationId } from '@hcengineering/server-notification'
@ -60,17 +61,36 @@ export function createModel (builder: Builder): void {
builder.createModel(THTMLPresenter, TTextPresenter, TTypeMatch, TNotificationPresenter) builder.createModel(THTMLPresenter, TTextPresenter, TTypeMatch, TNotificationPresenter)
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.OnBacklinkCreate trigger: serverNotification.trigger.ActivityNotificationsHandler,
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.NotificationMessagesHandler
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.OnChatMessageSent,
txMatch: { 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
} }
}) })

View File

@ -161,6 +161,24 @@ function defineFilters (builder: Builder): void {
getVisibleFilters: tracker.function.GetVisibleFilters 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 // Issue Status
// //
@ -256,6 +274,7 @@ function defineApplication (
alias: trackerId, alias: trackerId,
hidden: false, hidden: false,
locationResolver: tracker.resolver.Location, locationResolver: tracker.resolver.Location,
aside: chunter.component.ThreadView,
navigatorModel: { navigatorModel: {
specials: [ specials: [
{ {
@ -607,6 +626,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: tracker.class.Issue, objectClass: tracker.class.Issue,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },
@ -617,6 +637,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: tracker.class.IssueTemplate, objectClass: tracker.class.IssueTemplate,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },
@ -627,6 +648,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: tracker.class.Component, objectClass: tracker.class.Component,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },
@ -637,6 +659,7 @@ export function createModel (builder: Builder): void {
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
core.space.Model, core.space.Model,
{ {
messageClass: chunter.class.ChatMessage,
objectClass: tracker.class.Milestone, objectClass: tracker.class.Milestone,
label: chunter.string.LeftComment label: chunter.string.LeftComment
}, },

View File

@ -99,7 +99,9 @@ export default mergeIds(viewId, view, {
General: '' as IntlString, General: '' as IntlString,
Navigation: '' as IntlString, Navigation: '' as IntlString,
Editor: '' as IntlString, Editor: '' as IntlString,
MarkdownFormatting: '' as IntlString MarkdownFormatting: '' as IntlString,
Pin: '' as IntlString,
Unpin: '' as IntlString
}, },
function: { function: {
FilterArrayAllResult: '' as FilterFunction, FilterArrayAllResult: '' as FilterFunction,

View File

@ -22,8 +22,6 @@ import workbench from '@hcengineering/workbench-resources/src/plugin'
export default mergeIds(workbenchId, workbench, { export default mergeIds(workbenchId, workbench, {
component: { component: {
ApplicationPresenter: '' as AnyComponent, ApplicationPresenter: '' as AnyComponent,
Archive: '' as AnyComponent,
SpaceBrowser: '' as AnyComponent,
SpecialView: '' as AnyComponent, SpecialView: '' as AnyComponent,
ServerManager: '' as AnyComponent ServerManager: '' as AnyComponent
}, },

View File

@ -1,11 +1,23 @@
function isToday (time: number): boolean { //
const current = new Date() // Copyright © 2023 Hardcore Engineering Inc.
const target = new Date(time) //
return ( // Licensed under the Eclipse Public License, Version 2.0 (the "License");
current.getDate() === target.getDate() && // you may not use this file except in compliance with the License. You may
current.getMonth() === target.getMonth() && // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
current.getFullYear() === target.getFullYear() //
) // 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 { export function getDisplayTime (time: number): string {
@ -20,3 +32,17 @@ export function getDisplayTime (time: number): string {
return new Date(time).toLocaleString('default', options) 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()
)
}

View File

@ -94,7 +94,7 @@ export interface MigrationClient {
create: <T extends Doc>(domain: Domain, doc: T | T[]) => Promise<void> create: <T extends Doc>(domain: Domain, doc: T | T[]) => Promise<void>
delete: <T extends Doc>(domain: Domain, _id: Ref<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 hierarchy: Hierarchy
model: ModelDb model: ModelDb

View File

@ -48,6 +48,7 @@
dispatch('close') dispatch('close')
}} }}
/> />
<div class="mr-4" />
<Button <Button
label={presentation.string.Cancel} label={presentation.string.Cancel}
on:click={() => { on:click={() => {

View File

@ -64,6 +64,7 @@
on:action on:action
on:valid on:valid
on:validate on:validate
on:submit
> >
<slot /> <slot />
</Ctor> </Ctor>
@ -81,6 +82,7 @@
on:action on:action
on:valid on:valid
on:validate on:validate
on:submit
/> />
{/if} {/if}
</ErrorBoundary> </ErrorBoundary>

View File

@ -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" 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>
<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> </svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -17,12 +17,15 @@
"For": "For", "For": "For",
"From": "from", "From": "from",
"In": "In", "In": "In",
"LastReply": "Last reply",
"New": "New", "New": "New",
"NewestFirst": "Newest first", "NewestFirst": "Newest first",
"Pinned": "Pinned", "Pinned": "Pinned",
"Reacted": "Reacted", "Reacted": "Reacted",
"Reactions": "Reactions", "Reactions": "Reactions",
"Removed": "removed", "Removed": "removed",
"Replies": "Replies",
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
"Set": "set", "Set": "set",
"To": "to", "To": "to",
"Unset": "Unset", "Unset": "Unset",

View File

@ -17,12 +17,15 @@
"For": "Для", "For": "Для",
"From": "из", "From": "из",
"In": "В", "In": "В",
"LastReply": "Последний ответ",
"New": "Новые", "New": "Новые",
"NewestFirst": "Сначала новые", "NewestFirst": "Сначала новые",
"Pinned": "Закрепленные", "Pinned": "Закрепленные",
"Reacted": "Отреагировал(а)", "Reacted": "Отреагировал(а)",
"Reactions": "Реакции", "Reactions": "Реакции",
"Removed": "Удалил(а)", "Removed": "Удалил(а)",
"Replies": "Ответы",
"RepliesCount": "{replies, plural, one {# ответ} few {# ответа} other {# ответов}}",
"Set": "установлен", "Set": "установлен",
"To": "на", "To": "на",
"Unset": "Cбросил", "Unset": "Cбросил",

View File

@ -19,5 +19,6 @@ import { loadMetadata } from '@hcengineering/platform'
const icons = require('../assets/icons.svg') as string // eslint-disable-line const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(activity.icon, { loadMetadata(activity.icon, {
Activity: `${icons}#activity`, Activity: `${icons}#activity`,
Emoji: `${icons}#emoji` Emoji: `${icons}#emoji`,
Bookmark: `${icons}#bookmark`
}) })

View File

@ -39,7 +39,9 @@
"@hcengineering/contact": "^0.6.20", "@hcengineering/contact": "^0.6.20",
"@hcengineering/contact-resources": "^0.6.0", "@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/core": "^0.6.28", "@hcengineering/core": "^0.6.28",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9", "@hcengineering/platform": "^0.6.9",
"@hcengineering/preference": "^0.6.9",
"@hcengineering/presentation": "^0.6.2", "@hcengineering/presentation": "^0.6.2",
"@hcengineering/ui": "^0.6.11", "@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9", "@hcengineering/view": "^0.6.9",

View File

@ -22,18 +22,20 @@ import core, {
groupByArray, groupByArray,
type Hierarchy, type Hierarchy,
type Ref, type Ref,
SortingOrder SortingOrder,
type Timestamp
} from '@hcengineering/core' } from '@hcengineering/core'
import view, { type AttributeModel } from '@hcengineering/view' import view, { type AttributeModel } from '@hcengineering/view'
import { getClient, getFiltredKeys } from '@hcengineering/presentation' import { getClient, getFiltredKeys } from '@hcengineering/presentation'
import { buildRemovedDoc, getAttributePresenter, getDocLinkTitle } from '@hcengineering/view-resources' import { buildRemovedDoc, getAttributePresenter, getDocLinkTitle } from '@hcengineering/view-resources'
import { type Person } from '@hcengineering/contact' 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 { type AnyComponent } from '@hcengineering/ui'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import { personAccountByIdStore } from '@hcengineering/contact-resources' import { personAccountByIdStore } from '@hcengineering/contact-resources'
import activity, { import activity, {
type ActivityMessage, type ActivityMessage,
type ActivityMessagesFilter,
type DisplayActivityMessage, type DisplayActivityMessage,
type DisplayDocUpdateMessage, type DisplayDocUpdateMessage,
type DocAttributeUpdates, type DocAttributeUpdates,
@ -381,6 +383,10 @@ export function pinnedFilter (message: ActivityMessage, _class?: Ref<Doc>): bool
return message.isPinned === true return message.isPinned === true
} }
export function allFilter (): boolean {
return true
}
export interface LinkData { export interface LinkData {
title?: string title?: string
preposition: IntlString preposition: IntlString
@ -427,3 +433,57 @@ export async function getLinkData (
object: linkObject 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
}

View File

@ -24,5 +24,5 @@
</script> </script>
{#if extension} {#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} {/if}

View File

@ -30,12 +30,17 @@
export let object: Doc export let object: Doc
export let isNewestFirst = false export let isNewestFirst = false
let filtered: ActivityMessage[] const allId = activity.ids.AllFilter
const client = getClient() const client = getClient()
let filtered: ActivityMessage[]
let filters: ActivityMessagesFilter[] = [] let filters: ActivityMessagesFilter[] = []
const saved = localStorage.getItem('activity-filter') 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[] = [] let selectedFilters: ActivityMessagesFilter[] = []
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs)) $: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs))
@ -45,14 +50,14 @@
filters = res filters = res
if (saved !== null && saved !== undefined) { if (saved !== null && saved !== undefined) {
const temp: Ref<Doc>[] | 'All' = JSON.parse(saved) const temp: Ref<ActivityMessagesFilter>[] | Ref<ActivityMessagesFilter> = JSON.parse(saved)
if (temp !== 'All' && Array.isArray(temp)) { if (temp !== allId && Array.isArray(temp)) {
selectedFiltersRefs = temp.filter((it) => filters.findIndex((f) => it === f._id) > -1) selectedFiltersRefs = temp.filter((it) => filters.findIndex((f) => it === f._id) > -1)
if (selectedFiltersRefs.length === 0) { if (selectedFiltersRefs.length === 0) {
selectedFiltersRefs = 'All' selectedFiltersRefs = allId
} }
} else { } else {
selectedFiltersRefs = 'All' selectedFiltersRefs = allId
} }
} }
}) })
@ -69,9 +74,9 @@
isNewestFirst = res.value isNewestFirst = res.value
return return
} }
const selected = res.value as Ref<Doc>[] const selected = res.value as Ref<ActivityMessagesFilter>[]
const isAll = selected.length === filters.length || selected.length === 0 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 ( async function updateFilterActions (
messages: ActivityMessage[], messages: ActivityMessage[],
filters: ActivityMessagesFilter[], filters: ActivityMessagesFilter[],
selected: Ref<Doc>[] | 'All', selected: Ref<Doc>[] | Ref<ActivityMessagesFilter>,
sortOrder: SortingOrder sortOrder: SortingOrder
): Promise<void> { ): Promise<void> {
const sortedMessages = sortActivityMessages(messages, sortOrder).sort(({ isPinned }) => const sortedMessages = sortActivityMessages(messages, sortOrder).sort(({ isPinned }) =>
isPinned && sortOrder === SortingOrder.Ascending ? -1 : 1 isPinned && sortOrder === SortingOrder.Ascending ? -1 : 1
) )
if (selected === 'All') { if (selected === allId) {
filtered = sortedMessages filtered = sortedMessages
dispatch('update', filtered) dispatch('update', filtered)
@ -126,9 +131,9 @@
<div <div
class="tag-icon" class="tag-icon"
on:click={() => { on:click={() => {
if (selectedFiltersRefs !== 'All') { if (selectedFiltersRefs !== allId && Array.isArray(selectedFiltersRefs)) {
const ids = selectedFiltersRefs.filter((it) => it !== filter._id) const ids = selectedFiltersRefs.filter((it) => it !== filter._id)
selectedFiltersRefs = ids.length > 0 ? ids : 'All' selectedFiltersRefs = ids.length > 0 ? ids : allId
} }
}} }}
> >

View File

@ -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>

View File

@ -15,15 +15,17 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { Label, resizeObserver, CheckBox, MiniToggle } from '@hcengineering/ui' import { CheckBox, Label, MiniToggle, resizeObserver } from '@hcengineering/ui'
import { Doc, Ref } from '@hcengineering/core' import { Ref } from '@hcengineering/core'
import { ActivityMessagesFilter } from '@hcengineering/activity' import { ActivityMessagesFilter } from '@hcengineering/activity'
import activity from '../plugin' 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[] = [] export let filters: ActivityMessagesFilter[] = []
const allId = activity.ids.AllFilter
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false') let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
@ -31,20 +33,19 @@
interface ActionMenu { interface ActionMenu {
label: IntlString label: IntlString
checked: boolean checked: boolean
value: Ref<Doc> | 'All' value: Ref<ActivityMessagesFilter>
} }
let menu: ActionMenu[] = [
{ let menu: ActionMenu[] = []
label: activity.string.All,
checked: true, filters.map(({ label, _id }) => menu.push({ label, checked: _id === allId, value: _id }))
value: 'All'
} if (Array.isArray(selectedFiltersRefs)) {
] selectedFiltersRefs.forEach((filterId) => {
filters.map((fl) => menu.push({ label: fl.label, checked: false, value: fl._id })) const index = menu.findIndex(({ value }) => value === filterId)
if (selectedFiltersRefs !== 'All') { if (index !== -1) {
selectedFiltersRefs.forEach((fl) => { menu[index].checked = true
const index = menu.findIndex((el) => el.value === fl) }
if (index !== -1) menu[index].checked = true
}) })
} }
@ -79,31 +80,31 @@
const checkAll = () => { const checkAll = () => {
menu.forEach((el, i) => (el.checked = i === 0)) menu.forEach((el, i) => (el.checked = i === 0))
selectedFiltersRefs = 'All' selectedFiltersRefs = allId
} }
const uncheckAll = () => { const uncheckAll = () => {
menu.forEach((el) => (el.checked = true)) menu.forEach((el) => (el.checked = true))
const temp = filters.map((fl) => fl._id as Ref<Doc>) selectedFiltersRefs = filters.map(({ _id }) => _id)
selectedFiltersRefs = temp
} }
const selectRow = (n: number) => { const selectRow = (n: number) => {
if (n === 0) { if (n === 0) {
if (selectedFiltersRefs === 'All') uncheckAll() if (selectedFiltersRefs === allId) uncheckAll()
else checkAll() else checkAll()
} else { } else {
if (selectedFiltersRefs === 'All') { if (selectedFiltersRefs === allId) {
menu[n].checked = true menu[n].checked = true
selectedFiltersRefs = [menu[n].value as Ref<Doc>] selectedFiltersRefs = [menu[n].value]
} else if (menu[n].checked) { } else if (menu[n].checked) {
if (menu.filter((el) => el.checked).length === 2) checkAll() if (menu.filter((el) => el.checked).length === 2) checkAll()
else { else if (Array.isArray(selectedFiltersRefs)) {
menu[n].checked = false menu[n].checked = false
selectedFiltersRefs = selectedFiltersRefs.filter((fl) => fl !== menu[n].value) selectedFiltersRefs = selectedFiltersRefs.filter((fl) => fl !== menu[n].value)
} }
} else { } else if (Array.isArray(selectedFiltersRefs)) {
menu[n].checked = true menu[n].checked = true
selectedFiltersRefs.push(menu[n].value as Ref<Doc>) selectedFiltersRefs.push(menu[n].value)
} }
} }
menu = menu menu = menu
@ -152,7 +153,7 @@
}} }}
> >
<div class="flex-center justify-end mr-3 pointer-events-none"> <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> </div>
<span class="overflow-label"> <span class="overflow-label">
<Label label={item.label} /> <Label label={item.label} />

View File

@ -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"> <script lang="ts">
import { Timestamp } from '@hcengineering/core' import { getDay, Timestamp } from '@hcengineering/core'
import { DateRangePopup, showPopup } from '@hcengineering/ui' import { DateRangePopup, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { getDay } from '../utils'
export let selectedDate: Timestamp | undefined export let selectedDate: Timestamp | undefined
export let fixed: boolean = false export let fixed: boolean = false
@ -17,7 +30,6 @@
<div id={fixed ? '' : time?.toString()} class="flex-center clear-mins dateSelector"> <div id={fixed ? '' : time?.toString()} class="flex-center clear-mins dateSelector">
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
bind:this={div} bind:this={div}
class="border-radius-4 over-underline dateSelectorButton clear-mins" class="border-radius-4 over-underline dateSelectorButton clear-mins"

View 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>

View File

@ -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}
/>

View File

@ -36,7 +36,7 @@
export let isSelected: boolean = false export let isSelected: boolean = false
export let shouldScroll: boolean = false export let shouldScroll: boolean = false
export let embedded: boolean = false export let embedded: boolean = false
export let hasActionsMenu: boolean = true export let withActions: boolean = true
export let onClick: (() => void) | undefined = undefined export let onClick: (() => void) | undefined = undefined
$: personAccount = $personAccountByIdStore.get((value.createdBy ?? value.modifiedBy) as Ref<PersonAccount>) $: personAccount = $personAccountByIdStore.get((value.createdBy ?? value.modifiedBy) as Ref<PersonAccount>)
@ -65,7 +65,7 @@
{isSelected} {isSelected}
{shouldScroll} {shouldScroll}
{embedded} {embedded}
{hasActionsMenu} {withActions}
viewlet={undefined} viewlet={undefined}
{onClick} {onClick}
> >

View File

@ -16,7 +16,7 @@
import { DisplayActivityMessage } from '@hcengineering/activity' import { DisplayActivityMessage } from '@hcengineering/activity'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Component } from '@hcengineering/ui' import { Action, Component } from '@hcengineering/ui'
import { Class, Doc, Ref } from '@hcengineering/core' import { Class, Doc, Ref } from '@hcengineering/core'
export let value: DisplayActivityMessage export let value: DisplayActivityMessage
@ -25,8 +25,14 @@
export let isSelected: boolean = false export let isSelected: boolean = false
export let shouldScroll: boolean = false export let shouldScroll: boolean = false
export let embedded: 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 onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
@ -40,12 +46,17 @@
props={{ props={{
value, value,
showNotify, showNotify,
skipLabel,
isHighlighted, isHighlighted,
isSelected, isSelected,
shouldScroll, shouldScroll,
embedded, embedded,
hasActionsMenu, withActions,
onClick showEmbedded,
hideReplies,
actions,
onClick,
onReply
}} }}
/> />
{/if} {/if}

View File

@ -30,6 +30,8 @@
import ActivityMessageExtensionComponent from './ActivityMessageExtension.svelte' import ActivityMessageExtensionComponent from './ActivityMessageExtension.svelte'
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte' import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
import PinMessageAction from './PinMessageAction.svelte' import PinMessageAction from './PinMessageAction.svelte'
import Replies from '../Replies.svelte'
import SaveMessageAction from '../SaveMessageAction.svelte'
export let message: DisplayActivityMessage export let message: DisplayActivityMessage
export let parentMessage: DisplayActivityMessage | undefined export let parentMessage: DisplayActivityMessage | undefined
@ -43,8 +45,11 @@
export let isSelected: boolean = false export let isSelected: boolean = false
export let shouldScroll: boolean = false export let shouldScroll: boolean = false
export let embedded: 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 onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient() const client = getClient()
let allActionIds: string[] = [] let allActionIds: string[] = []
@ -53,9 +58,10 @@
let extensions: ActivityMessageExtension[] = [] let extensions: ActivityMessageExtension[] = []
let isActionMenuOpened = false let isActionMenuOpened = false
$: void getActions(client, message, activity.class.ActivityMessage).then((res) => { $: withActions &&
allActionIds = res.map(({ _id }) => _id) getActions(client, message, activity.class.ActivityMessage).then((res) => {
}) allActionIds = res.map(({ _id }) => _id)
})
function scrollToMessage (): void { function scrollToMessage (): void {
if (element != null && shouldScroll) { if (element != null && shouldScroll) {
@ -101,7 +107,7 @@
$: isHidden = !!viewlet?.onlyWithParent && parentMessage === undefined $: isHidden = !!viewlet?.onlyWithParent && parentMessage === undefined
$: withActionMenu = $: withActionMenu =
!embedded && hasActionsMenu && (actions.length > 0 || allActionIds.some((id) => !excludedActions.includes(id))) withActions && !embedded && (actions.length > 0 || allActionIds.some((id) => !excludedActions.includes(id)))
</script> </script>
{#if !isHidden} {#if !isHidden}
@ -151,32 +157,38 @@
<slot name="content" /> <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 }} /> <ActivityMessageExtensionComponent kind="footer" {extensions} props={{ object: message }} />
<ReactionsPresenter object={message} /> <ReactionsPresenter object={message} />
{#if parentMessage && showEmbedded}
{#if parentMessage}
<div class="mt-2" /> <div class="mt-2" />
<ActivityMessagePresenter value={parentMessage} embedded /> <ActivityMessagePresenter value={parentMessage} embedded hideReplies withActions={false} />
{/if} {/if}
</div> </div>
<div <div
class="actions clear-mins flex flex-gap-2 items-center" class="actions clear-mins flex flex-gap-2 items-center"
class:menuShowed={isActionMenuOpened || message.isPinned} class:opened={isActionMenuOpened || message.isPinned}
> >
<AddReactionAction object={message} /> {#if withActions}
<PinMessageAction object={message} /> <AddReactionAction object={message} />
<PinMessageAction object={message} />
<SaveMessageAction object={message} />
<ActivityMessageExtensionComponent <ActivityMessageExtensionComponent
kind="action" kind="action"
{extensions} {extensions}
props={{ object: message }} props={{ object: message }}
on:close={handleActionMenuClosed} on:close={handleActionMenuClosed}
on:open={handleActionMenuOpened} on:open={handleActionMenuOpened}
/> />
{#if withActionMenu} {#if withActionMenu}
<ActionIcon icon={IconMoreH} size="small" action={showMenu} /> <ActionIcon icon={IconMoreH} size="small" action={showMenu} />
{/if}
{/if} {/if}
</div> </div>
</div> </div>
@ -195,8 +207,8 @@
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
padding: 0.75rem 0.75rem 0.75rem 1.25rem; padding: 0.75rem 0.75rem 0.75rem 1.25rem;
border-radius: 8px;
gap: 1rem; gap: 1rem;
overflow: hidden;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 0.25rem; border-radius: 0.25rem;
@ -228,7 +240,7 @@
right: 0.75rem; right: 0.75rem;
color: var(--theme-halfcontent-color); color: var(--theme-halfcontent-color);
&.menuShowed { &.opened {
visibility: visible; visibility: visible;
} }
} }

View File

@ -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"); // 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 // 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 params: any = undefined
export let reverse: boolean = false export let reverse: boolean = false
export let isNew: boolean = false export let isNew: boolean = false
export let element: HTMLDivElement | undefined = undefined
</script> </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-8={!reverse} class:mr-4={reverse}><Label label={title} {params} /></div>
<div class:ml-4={!reverse} class:mr-4={reverse} class:line /> <div class:ml-4={!reverse} class:mr-4={reverse} class:line />
</div> </div>
@ -35,6 +41,7 @@
height: 1px; height: 1px;
background-color: var(--divider-color); background-color: var(--divider-color);
} }
.new { .new {
.line { .line {
background-color: var(--highlight-red); background-color: var(--highlight-red);

View File

@ -25,7 +25,7 @@
import core, { Account, AttachedDoc, Class, Collection, Doc, Ref } from '@hcengineering/core' import core, { Account, AttachedDoc, Class, Collection, Doc, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' 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 { AttributeModel } from '@hcengineering/view'
import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte' import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte'
@ -42,8 +42,12 @@
export let isSelected: boolean = false export let isSelected: boolean = false
export let shouldScroll: boolean = false export let shouldScroll: boolean = false
export let embedded: 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 onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
@ -146,9 +150,13 @@
{isSelected} {isSelected}
{shouldScroll} {shouldScroll}
{embedded} {embedded}
{hasActionsMenu} {withActions}
{viewlet} {viewlet}
{showEmbedded}
{hideReplies}
{actions}
{onClick} {onClick}
{onReply}
> >
<svelte:fragment slot="header"> <svelte:fragment slot="header">
{#if viewlet?.labelComponent} {#if viewlet?.labelComponent}

View File

@ -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>

View File

@ -16,12 +16,11 @@
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { ActionIcon, EmojiPopup, IconEmoji, showPopup } from '@hcengineering/ui' import { ActionIcon, EmojiPopup, IconEmoji, showPopup } from '@hcengineering/ui'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import activity, { Reaction } from '@hcengineering/activity' import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
import { Doc } from '@hcengineering/core'
import { updateDocReactions } from '../../utils' import { updateDocReactions } from '../../utils'
export let object: Doc | undefined = undefined export let object: ActivityMessage | undefined = undefined
const client = getClient() const client = getClient()
@ -38,16 +37,10 @@
function openEmojiPalette (ev: Event) { function openEmojiPalette (ev: Event) {
dispatch('open') dispatch('open')
showPopup( showPopup(EmojiPopup, {}, ev.target as HTMLElement, (emoji: string) => {
EmojiPopup, updateDocReactions(client, reactions, object, emoji)
{}, dispatch('close')
ev.target as HTMLElement, })
(emoji: string) => {
updateDocReactions(client, reactions, object, emoji)
dispatch('close')
},
() => {}
)
} }
</script> </script>

View File

@ -14,13 +14,12 @@
--> -->
<script lang="ts"> <script lang="ts">
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import activity, { Reaction } from '@hcengineering/activity' import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
import { Doc } from '@hcengineering/core'
import Reactions from './Reactions.svelte' import Reactions from './Reactions.svelte'
import { updateDocReactions } from '../../utils' import { updateDocReactions } from '../../utils'
export let object: Doc | undefined export let object: ActivityMessage | undefined
const client = getClient() const client = getClient()
const reactionsQuery = createQuery() const reactionsQuery = createQuery()
@ -38,7 +37,7 @@
} }
</script> </script>
{#if reactions.length} {#if object !== undefined && reactions.length > 0}
<div class="footer flex-col p-inline contrast mt-2"> <div class="footer flex-col p-inline contrast mt-2">
<Reactions {reactions} {object} on:click={handleClick} /> <Reactions {reactions} {object} on:click={handleClick} />
</div> </div>

View File

@ -21,7 +21,7 @@ import DocUpdateMessagePresenter from './components/doc-update-message/DocUpdate
import ActivityInfoMessagePresenter from './components/activity-message/ActivityInfoMessagePresenter.svelte' import ActivityInfoMessagePresenter from './components/activity-message/ActivityInfoMessagePresenter.svelte'
import ReactionAddedMessage from './components/reactions/ReactionAddedMessage.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 './activity'
export * from './utils' 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 ActivityMessageTemplate } from './components/activity-message/ActivityMessageTemplate.svelte'
export { default as ActivityMessagePresenter } from './components/activity-message/ActivityMessagePresenter.svelte' export { default as ActivityMessagePresenter } from './components/activity-message/ActivityMessagePresenter.svelte'
export { default as ActivityExtension } from './components/ActivityExtension.svelte' export { default as ActivityExtension } from './components/ActivityExtension.svelte'
export { default as ActivityScrolledView } from './components/ActivityScrolledView.svelte'
export { default as ActivityMessageHeader } from './components/activity-message/ActivityMessageHeader.svelte' export { default as ActivityMessageHeader } from './components/activity-message/ActivityMessageHeader.svelte'
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
@ -43,6 +44,10 @@ export default async (): Promise<Resources> => ({
}, },
filter: { filter: {
AttributesFilter: attributesFilter, AttributesFilter: attributesFilter,
PinnedFilter: pinnedFilter PinnedFilter: pinnedFilter,
AllFilter: allFilter
},
function: {
GetFragment: getMessageFragment
} }
}) })

View File

@ -29,8 +29,11 @@
"typescript": "^5.2.2" "typescript": "^5.2.2"
}, },
"dependencies": { "dependencies": {
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28", "@hcengineering/core": "^0.6.28",
"@hcengineering/platform": "^0.6.9", "@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"
} }
} }

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { Person } from '@hcengineering/contact'
import { import {
Account, Account,
AttachedDoc, AttachedDoc,
@ -32,6 +33,7 @@ import {
} from '@hcengineering/core' } from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import { Preference } from '@hcengineering/preference'
import type { AnyComponent } from '@hcengineering/ui' import type { AnyComponent } from '@hcengineering/ui'
// TODO: remove TxViewlet // TODO: remove TxViewlet
@ -110,10 +112,14 @@ export interface ActivityMessage extends AttachedDoc {
isPinned?: boolean isPinned?: boolean
repliedPersons?: Ref<Person>[]
lastReply?: Timestamp
replies?: number
reactions?: number reactions?: number
} }
export type DisplayActivityMessage = DisplayDocUpdateMessage | ActivityMessage | ActivityInfoMessage export type DisplayActivityMessage = DisplayDocUpdateMessage | ActivityMessage
export interface DisplayDocUpdateMessage extends DocUpdateMessage { export interface DisplayDocUpdateMessage extends DocUpdateMessage {
previousMessages?: DocUpdateMessage[] previousMessages?: DocUpdateMessage[]
@ -228,6 +234,7 @@ export const activityId = 'activity' as Plugin
*/ */
export interface ActivityMessagesFilter extends Doc { export interface ActivityMessagesFilter extends Doc {
label: IntlString label: IntlString
position: number
filter: Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean> filter: Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
} }
@ -262,10 +269,19 @@ export interface ActivityExtension extends Doc {
* @public * @public
*/ */
export interface Reaction extends AttachedDoc { export interface Reaction extends AttachedDoc {
attachedTo: Ref<ActivityMessage>
attachedToClass: Ref<Class<ActivityMessage>>
emoji: string emoji: string
createBy: Ref<Account> createBy: Ref<Account>
} }
/**
* @public
*/
export interface SavedMessage extends Preference {
attachedTo: Ref<ActivityMessage>
}
/** /**
* @public * @public
*/ */
@ -287,11 +303,13 @@ export default plugin(activityId, {
ActivityMessageExtension: '' as Ref<Class<ActivityMessageExtension>>, ActivityMessageExtension: '' as Ref<Class<ActivityMessageExtension>>,
ActivityMessagesFilter: '' as Ref<Class<ActivityMessagesFilter>>, ActivityMessagesFilter: '' as Ref<Class<ActivityMessagesFilter>>,
ActivityExtension: '' as Ref<Class<ActivityExtension>>, ActivityExtension: '' as Ref<Class<ActivityExtension>>,
Reaction: '' as Ref<Class<Reaction>> Reaction: '' as Ref<Class<Reaction>>,
SavedMessage: '' as Ref<Class<SavedMessage>>
}, },
icon: { icon: {
Activity: '' as Asset, Activity: '' as Asset,
Emoji: '' as Asset Emoji: '' as Asset,
Bookmark: '' as Asset
}, },
string: { string: {
Activity: '' as IntlString, Activity: '' as IntlString,
@ -313,7 +331,9 @@ export default plugin(activityId, {
Update: '' as IntlString, Update: '' as IntlString,
For: '' as IntlString, For: '' as IntlString,
AllActivity: '' as IntlString, AllActivity: '' as IntlString,
Reactions: '' as IntlString Reactions: '' as IntlString,
LastReply: '' as IntlString,
RepliesCount: '' as IntlString
}, },
component: { component: {
Activity: '' as AnyComponent, Activity: '' as AnyComponent,
@ -321,5 +341,8 @@ export default plugin(activityId, {
DocUpdateMessagePresenter: '' as AnyComponent, DocUpdateMessagePresenter: '' as AnyComponent,
ActivityInfoMessagePresenter: '' as AnyComponent, ActivityInfoMessagePresenter: '' as AnyComponent,
ReactionAddedMessage: '' as AnyComponent ReactionAddedMessage: '' as AnyComponent
},
ids: {
AllFilter: '' as Ref<ActivityMessagesFilter>
} }
}) })

View File

@ -14,14 +14,18 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Attachment } from '@hcengineering/attachment' import { Attachment } from '@hcengineering/attachment'
import type { Doc } from '@hcengineering/core' import type { Doc, Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import attachment from '../plugin' import attachment from '../plugin'
import AttachmentList from './AttachmentList.svelte' import AttachmentList from './AttachmentList.svelte'
export let value: Doc & { attachments?: number } export let value: Doc & { attachments?: number }
const query = createQuery() const query = createQuery()
const savedAttachmentsQuery = createQuery()
let savedAttachmentsIds: Ref<Attachment>[] = []
let attachments: Attachment[] = [] let attachments: Attachment[] = []
$: updateQuery(value) $: updateQuery(value)
@ -41,6 +45,10 @@
attachments = [] attachments = []
} }
} }
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
savedAttachmentsIds = res.map(({ attachedTo }) => attachedTo)
})
</script> </script>
<AttachmentList {attachments} /> <AttachmentList {attachments} {savedAttachmentsIds} />

View File

@ -24,7 +24,6 @@ export default mergeIds(attachmentId, attachment, {
NoAttachments: '' as IntlString, NoAttachments: '' as IntlString,
UploadDropFilesHere: '' as IntlString, UploadDropFilesHere: '' as IntlString,
Photos: '' as IntlString, Photos: '' as IntlString,
FileBrowser: '' as IntlString,
FileBrowserFileCounter: '' as IntlString, FileBrowserFileCounter: '' as IntlString,
FileBrowserListView: '' as IntlString, FileBrowserListView: '' as IntlString,
FileBrowserGridView: '' as IntlString, FileBrowserGridView: '' as IntlString,

View File

@ -56,7 +56,8 @@ export default plugin(attachmentId, {
component: { component: {
Attachments: '' as AnyComponent, Attachments: '' as AnyComponent,
Photos: '' as AnyComponent, Photos: '' as AnyComponent,
AttachmentsPresenter: '' as AnyComponent AttachmentsPresenter: '' as AnyComponent,
FileBrowser: '' as AnyComponent
}, },
icon: { icon: {
Attachment: '' as Asset, Attachment: '' as Asset,
@ -89,6 +90,7 @@ export default plugin(attachmentId, {
FileBrowserTypeFilterVideos: '' as IntlString, FileBrowserTypeFilterVideos: '' as IntlString,
FileBrowserTypeFilterPDFs: '' as IntlString, FileBrowserTypeFilterPDFs: '' as IntlString,
DeleteFile: '' as IntlString, DeleteFile: '' as IntlString,
Attachments: '' as IntlString Attachments: '' as IntlString,
FileBrowser: '' as IntlString
} }
}) })

View File

@ -15,14 +15,14 @@
<script lang="ts"> <script lang="ts">
import { Doc } from '@hcengineering/core' import { Doc } from '@hcengineering/core'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { NotificationClientImpl } from '@hcengineering/notification-resources' import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { Icon, IconSize } from '@hcengineering/ui' import { Icon, IconSize } from '@hcengineering/ui'
export let object: Doc export let object: Doc
export let size: IconSize = 'small' export let size: IconSize = 'small'
const notificationClient = NotificationClientImpl.getClient() const inboxClient = InboxNotificationsClientImpl.getClient()
const store = notificationClient.docUpdatesStore const store = inboxClient.docNotifyContextByDoc
$: subscribed = $store.get(object._id) !== undefined $: subscribed = $store.get(object._id) !== undefined
</script> </script>

View File

@ -11,9 +11,6 @@
<symbol id="thread" viewBox="0 0 24 24"> <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" /> <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>
<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"> <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="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" /> <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

View File

@ -29,9 +29,6 @@
"Chat": "Chat", "Chat": "Chat",
"In": "In", "In": "In",
"MentionNotification": "Mentioned", "MentionNotification": "Mentioned",
"Replies": "Replies",
"LastReply": "Last reply",
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
"Topic": "Topic", "Topic": "Topic",
"Thread": "Thread", "Thread": "Thread",
"Threads": "Threads", "Threads": "Threads",
@ -80,6 +77,13 @@
"DirectNotificationTitle": "{senderName}", "DirectNotificationTitle": "{senderName}",
"DirectNotificationBody": "{message}", "DirectNotificationBody": "{message}",
"AddCommentPlaceholder": "Add comment...", "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"
} }
} }

View File

@ -29,9 +29,6 @@
"Reference": "Ссылка", "Reference": "Ссылка",
"Chat": "Чат", "Chat": "Чат",
"In": "в", "In": "в",
"Replies": "Ответы",
"LastReply": "Последний ответ",
"RepliesCount": "{replies, plural, one {# ответ} few {# ответа} other {# ответов}}",
"Topic": "Топик", "Topic": "Топик",
"Thread": "Обсуждение", "Thread": "Обсуждение",
"Threads": "Обсуждения", "Threads": "Обсуждения",
@ -80,6 +77,13 @@
"DirectNotificationTitle": "{senderName}", "DirectNotificationTitle": "{senderName}",
"DirectNotificationBody": "{message}", "DirectNotificationBody": "{message}",
"AddCommentPlaceholder": "Добавить комментарий...", "AddCommentPlaceholder": "Добавить комментарий...",
"Reacted": "Отреагировал(а)" "Reacted": "Отреагировал(а)",
"Saved": "Сохранено",
"Docs": "Documents",
"NewestFirst": "Сначала новые",
"ReplyToThread": "Ответить в канале",
"SentMessage": "Отправил(а) сообщение",
"Direct": "личные сообщения",
"RepliedToThread": "Ответил(а) в канале"
} }
} }

View File

@ -22,6 +22,5 @@ loadMetadata(chunter.icon, {
Hashtag: `${icons}#hashtag`, Hashtag: `${icons}#hashtag`,
Thread: `${icons}#thread`, Thread: `${icons}#thread`,
Lock: `${icons}#lock`, Lock: `${icons}#lock`,
Bookmark: `${icons}#bookmark`,
ChannelBrowser: `${icons}#channelbrowser` ChannelBrowser: `${icons}#channelbrowser`
}) })

View File

@ -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"); // 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 // you may not use this file except in compliance with the License. You may
@ -13,228 +13,39 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment' import { Doc, Ref } from '@hcengineering/core'
import type { ChunterMessage, Message } from '@hcengineering/chunter' import { DocNotifyContext } from '@hcengineering/notification'
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 { location as locationStore } from '@hcengineering/ui' import { location as locationStore } from '@hcengineering/ui'
import { afterUpdate, beforeUpdate, onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import activity from '@hcengineering/activity' import activity, { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity'
import { ActivityScrolledView } from '@hcengineering/activity-resources'
import chunter from '../plugin' 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 notifyContext: DocNotifyContext
export let pinnedIds: Ref<ChunterMessage>[] export let object: Doc
export let savedMessagesIds: Ref<ChunterMessage>[] export let filterId: Ref<ActivityMessagesFilter> = activity.ids.AllFilter
export let savedAttachmentsIds: Ref<Attachment>[]
export let isScrollForced = false
export let content: HTMLElement | undefined = undefined
let autoscroll: boolean = false let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
const unsubscribe = locationStore.subscribe((newLocation) => { const unsubscribe = locationStore.subscribe((newLocation) => {
const messageId = newLocation.fragment selectedMessageId = newLocation.fragment as Ref<ActivityMessage>
if (!messageId) {
messageIdForScroll.set('')
return
}
if (messageId === $messageIdForScroll) {
return
}
messageIdForScroll.set(messageId)
shouldScrollToMessage.set(true)
scrollAndHighLight()
}) })
onDestroy(unsubscribe) onDestroy(unsubscribe)
beforeUpdate(() => { $: isDocChannel = ![chunter.class.DirectMessage, chunter.class.Channel].includes(notifyContext.attachedToClass)
autoscroll = content !== undefined && content.offsetHeight + content.scrollTop > content.scrollHeight - 20 $: messagesClass = isDocChannel ? activity.class.ActivityMessage : chunter.class.ChatMessage
}) $: collection = isDocChannel ? 'comments' : 'messages'
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
}
</script> </script>
<div class="flex-col vScroll" bind:this={content} on:scroll={handleScroll}> <ActivityScrolledView
<div class="grower" /> _class={messagesClass}
{#if showFixed} {object}
<div class="ml-2 pr-2 fixed"> skipLabels={!isDocChannel}
<JumpToDateSelector {selectedDate} fixed on:jumpToDate={handleJumpToDate} /> filter={filterId}
</div> startFromBottom
{/if} {selectedMessageId}
{#if messages} {collection}
{#each messages as message, i (message._id)} lastViewedTimestamp={notifyContext.lastViewedTimestamp}
{#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>

View File

@ -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"); // 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 // you may not use this file except in compliance with the License. You may
@ -13,38 +13,38 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <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 { 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 client = getClient()
const query = createQuery() const hierarchy = client.getHierarchy()
let channel: Channel | undefined
$: query.query(chunter.class.Channel, { _id: spaceId }, (result) => { $: topic = hierarchy.isDerived(object._class, chunter.class.Channel) ? (object as Channel).topic : undefined
channel = result[0]
})
async function onSpaceEdit (): Promise<void> { async function getTitle (object: Doc) {
if (channel === undefined) return if (object._class === chunter.class.DirectMessage) {
openDoc(client.getHierarchy(), channel) return await getDocTitle(client, object._id, object._class, object)
}
return await getDocLinkTitle(client, object._id, object._class, object)
} }
</script> </script>
<div class="ac-header divide full caption-height"> <div class="ac-header divide full caption-height">
{#if channel} {#await getTitle(object) then title}
<Header <Header
icon={channel.private ? Lock : classIcon(client, channel._class)} icon={getChannelIcon(object)}
label={channel.name} iconProps={{ value: object }}
description={channel.topic} label={title}
on:click={onSpaceEdit} intlLabel={title ? undefined : chunter.string.Channel}
description={topic}
/> />
{/if} {/await}
</div> </div>

View File

@ -15,22 +15,20 @@
<script lang="ts"> <script lang="ts">
import { SortingOrder } from '@hcengineering/core' import { SortingOrder } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import chunter, { ChunterMessage, DirectMessage } from '@hcengineering/chunter' import chunter, { ChatMessage, DirectMessage } from '@hcengineering/chunter'
import attachment from '@hcengineering/attachment'
import { Label } from '@hcengineering/ui' import { Label } from '@hcengineering/ui'
import { ActivityMessagePresenter } from '@hcengineering/activity-resources'
import chunterResources from '../plugin' import chunterResources from '../plugin'
import MessagePreview from './MessagePreview.svelte'
export let object: DirectMessage export let object: DirectMessage
export let newTxes: number
const NUM_OF_RECENT_MESSAGES = 5 as const const NUM_OF_RECENT_MESSAGES = 5 as const
let messages: ChunterMessage[] = [] let messages: ChatMessage[] = []
const messagesQuery = createQuery() const messagesQuery = createQuery()
$: messagesQuery.query( $: messagesQuery.query(
chunter.class.ChunterMessage, chunter.class.ChatMessage,
{ attachedTo: object._id }, { attachedTo: object._id },
(res) => { (res) => {
if (res !== undefined) { if (res !== undefined) {
@ -38,12 +36,9 @@
} }
}, },
{ {
limit: newTxes + NUM_OF_RECENT_MESSAGES, limit: NUM_OF_RECENT_MESSAGES,
sort: { sort: {
createdOn: SortingOrder.Descending createdOn: SortingOrder.Descending
},
lookup: {
_id: { attachments: attachment.class.Attachment }
} }
} }
) )
@ -52,7 +47,7 @@
<div class="flex-col flex-gap-3 preview-container"> <div class="flex-col flex-gap-3 preview-container">
{#if messages.length} {#if messages.length}
{#each messages as message} {#each messages as message}
<MessagePreview value={message} /> <ActivityMessagePresenter value={message} skipLabel />
{/each} {/each}
{:else} {:else}
<Label label={chunterResources.string.NoMessages} /> <Label label={chunterResources.string.NoMessages} />

View File

@ -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"); // 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 // you may not use this file except in compliance with the License. You may
@ -13,97 +13,34 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment' import { Ref, Doc } from '@hcengineering/core'
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 { getLocation, navigate } from '@hcengineering/ui' 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 Channel from './Channel.svelte'
import PinnedMessages from './PinnedMessages.svelte' import PinnedMessages from './PinnedMessages.svelte'
import ChannelHeader from './ChannelHeader.svelte'
export let space: Ref<Space> export let notifyContext: DocNotifyContext
let chunterSpace: ChunterSpace export let object: Doc
let isScrollForced = false export let filterId: Ref<ActivityMessagesFilter> = activity.ids.AllFilter
const client = getClient() function openThread (_id: Ref<ChatMessage>) {
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>) {
const loc = getLocation() const loc = getLocation()
loc.path[4] = _id loc.path[4] = _id
navigate(loc) 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> </script>
<PinnedMessages {space} {pinnedIds} /> <ChannelHeader {object} />
<PinnedMessages {notifyContext} />
<Channel <Channel
bind:isScrollForced {notifyContext}
bind:content {object}
{space} {filterId}
on:openThread={(e) => { on:openThread={(e) => {
openThread(e.detail) 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>

View File

@ -13,14 +13,32 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Ref, Space } from '@hcengineering/core' import { Doc, Ref } from '@hcengineering/core'
import ChannelView from './ChannelView.svelte' import notification, { DocNotifyContext } from '@hcengineering/notification'
import SpaceHeader from './SpaceHeader.svelte' 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> </script>
<div class="antiComponent"> {#if notifyContext && object}
<SpaceHeader spaceId={_id} withSearch={false} /> <div class="antiComponent">
<ChannelView space={_id} /> <ChannelPresenter {notifyContext} {object} />
</div> </div>
{/if}

View File

@ -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}

View File

@ -15,28 +15,22 @@
<script lang="ts"> <script lang="ts">
import { chunterId, DirectMessage } from '@hcengineering/chunter' import { chunterId, DirectMessage } from '@hcengineering/chunter'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Icon } from '@hcengineering/ui'
import { NavLink } from '@hcengineering/view-resources' import { NavLink } from '@hcengineering/view-resources'
import { getDmName } from '../utils' import { getDmName } from '../utils'
import DmIconPresenter from './DmIconPresenter.svelte'
export let value: DirectMessage export let value: DirectMessage
export let disabled = false export let disabled = false
const client = getClient() const client = getClient()
$: icon = client.getHierarchy().getClass(value._class).icon
</script> </script>
{#if value} {#if value}
{#await getDmName(client, value) then name} {#await getDmName(client, value) then name}
<NavLink app={chunterId} space={value._id} {disabled}> <NavLink app={chunterId} space={value._id} {disabled}>
<div class="flex-presenter"> <div class="flex-presenter">
<div class="icon"> <DmIconPresenter {value} />
{#if icon}
<Icon {icon} size={'small'} />
{/if}
</div>
<span class="label">{name}</span> <span class="label">{name}</span>
</div> </div>
</NavLink> </NavLink>

View File

@ -19,6 +19,7 @@
import { navigateToSpecial } from '../utils' import { navigateToSpecial } from '../utils'
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined
export let label: string | undefined = undefined export let label: string | undefined = undefined
export let intlLabel: IntlString | undefined = undefined export let intlLabel: IntlString | undefined = undefined
export let description: string | undefined = undefined export let description: string | undefined = undefined
@ -31,7 +32,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="ac-header__wrap-title" on:click> <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} {#if label}
<span class="ac-header__title">{label}</span> <span class="ac-header__title">{label}</span>
{:else if intlLabel} {:else if intlLabel}

View File

@ -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>

View File

@ -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>
&nbsp;
{/if}
<svelte:component this={presenter.presenter} value={doc} {inline} {disabled} shouldShowAvatarr={false} />
</div>
{/if}
{:else}
<div><MessageViewer message={value.content} /></div>
{/if}

View File

@ -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>

View File

@ -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}

View File

@ -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"> <script lang="ts">
import { ChunterMessage } from '@hcengineering/chunter'
import { Ref, Space } from '@hcengineering/core'
import { eventToHTMLElement, Label, showPopup } from '@hcengineering/ui' import { eventToHTMLElement, Label, showPopup } from '@hcengineering/ui'
import PinnedMessagesPopup from './PinnedMessagesPopup.svelte' 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' import chunter from '../plugin'
export let space: Ref<Space> export let notifyContext: DocNotifyContext
export let pinnedIds: Ref<ChunterMessage>[]
function showMessages (ev: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) { const pinnedQuery = createQuery()
showPopup(PinnedMessagesPopup, { space }, eventToHTMLElement(ev))
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> </script>
{#if pinnedIds.length > 0} {#if pinnedMessagesCount > 0}
<div class="bottom-divider over-underline pt-2 pb-2 container"> <div class="bottom-divider over-underline pt-2 pb-2 container">
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div on:click={openMessagesPopup}>
on:click={(ev) => {
showMessages(ev)
}}
>
<Label label={chunter.string.Pinned} /> <Label label={chunter.string.Pinned} />
{pinnedIds.length} {pinnedMessagesCount}
</div> </div>
</div> </div>
{/if} {/if}

View File

@ -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"> <script lang="ts">
import chunter, { ChunterMessage } from '@hcengineering/chunter' import { Doc, Ref } from '@hcengineering/core'
import { Person, PersonAccount, getName } from '@hcengineering/contact' import { createQuery } from '@hcengineering/presentation'
import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources' import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import { getDisplayTime, IdMap, Ref, Space } from '@hcengineering/core' import { ActivityMessagePresenter } from '@hcengineering/activity-resources'
import { MessageViewer, createQuery, getClient } from '@hcengineering/presentation'
import { IconClose } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { UnpinMessage } from '../index'
export let space: Ref<Space> export let attachedTo: Ref<Doc>
const client = getClient()
const pinnedQuery = createQuery()
let pinnedIds: Ref<ChunterMessage>[] = []
pinnedQuery.query(
chunter.class.ChunterSpace,
{ _id: space },
(res) => {
pinnedIds = res[0]?.pinned ?? []
},
{ limit: 1 }
)
const messagesQuery = createQuery() const messagesQuery = createQuery()
let pinnedMessages: ChunterMessage[] = []
$: pinnedIds && let pinnedMessages: DisplayActivityMessage[] = []
messagesQuery.query(chunter.class.ChunterMessage, { _id: { $in: pinnedIds } }, (res) => {
pinnedMessages = res
})
const dispatch = createEventDispatcher() $: messagesQuery.query(activity.class.ActivityMessage, { attachedTo, isPinned: true }, (res: ActivityMessage[]) => {
pinnedMessages = res as DisplayActivityMessage[]
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)
}
}
</script> </script>
<div class="antiPopup vScroll popup"> <div class="antiPopup vScroll popup">
{#each pinnedMessages as message} {#each pinnedMessages as message}
{@const employee = getEmployee(message, $personAccountByIdStore, $personByIdStore)} <ActivityMessagePresenter value={message} withActions={false} />
<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>
{/each} {/each}
</div> </div>
@ -80,34 +41,4 @@
max-height: 20rem; max-height: 20rem;
color: var(--caption-color); 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> </style>

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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} />

View File

@ -21,11 +21,14 @@
import { getLinkData, LinkData } from '@hcengineering/activity-resources' import { getLinkData, LinkData } from '@hcengineering/activity-resources'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import chunter from '../../plugin'
export let message: ChatMessage export let message: ChatMessage
export let person: Person | undefined export let person: Person | undefined
export let viewlet: ChatMessageViewlet | undefined export let viewlet: ChatMessageViewlet | undefined
export let object: Doc | undefined export let object: Doc | undefined
export let parentObject: Doc | undefined export let parentObject: Doc | undefined
export let skipLabel = false
let linkData: LinkData | undefined = undefined let linkData: LinkData | undefined = undefined
@ -34,21 +37,21 @@
}) })
</script> </script>
{#if viewlet?.label} {#if !skipLabel}
<span class="text-sm lower"> <Label label={viewlet.label} /></span> <span class="text-sm lower"> <Label label={viewlet?.label ?? chunter.string.SentMessage} /></span>
{#if linkData} {#if linkData}
<span class="text-sm lower"><Label label={linkData.preposition} /></span> <span class="text-sm lower"><Label label={linkData.preposition} /></span>
<span class="text-sm"> <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> <span class="overflow-label select-text">{linkData.title}</span>
</DocNavLink> </DocNavLink>
</span> </span>
{#if message.isEdited}
<span class="text-sm lower"><Label label={notification.string.Edited} /></span>
{/if}
{/if} {/if}
{/if} {/if}
{#if message.editedOn}
<span class="text-sm lower"><Label label={notification.string.Edited} /></span>
{/if}
<style lang="scss"> <style lang="scss">
span { span {

View File

@ -15,24 +15,31 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { AttachmentRefInput } from '@hcengineering/attachment-resources' 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 { 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 object: Doc
export let chatMessage: ChatMessage | undefined = undefined export let chatMessage: ChatMessage | undefined = undefined
export let shouldSaveDraft: boolean = true export let shouldSaveDraft: boolean = true
export let focusIndex: number = -1 export let focusIndex: number = -1
export let boundary: HTMLElement | undefined = undefined export let boundary: HTMLElement | undefined = undefined
export let loading = false
export let collection: string = 'comments'
type MessageDraft = Pick<ChatMessage, '_id' | 'message' | 'attachments'> type MessageDraft = Pick<ChatMessage, '_id' | 'message' | 'attachments'>
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() 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 createdMessageQuery = createQuery()
const account = getCurrentAccount() const account = getCurrentAccount() as PersonAccount
const draftKey = `${object._id}_${_class}` const draftKey = `${object._id}_${_class}`
const draftController = new DraftController<MessageDraft>(draftKey) const draftController = new DraftController<MessageDraft>(draftKey)
@ -47,9 +54,8 @@
let currentMessage: MessageDraft = chatMessage ?? currentDraft ?? getDefault() let currentMessage: MessageDraft = chatMessage ?? currentDraft ?? getDefault()
let _id = currentMessage._id let _id = currentMessage._id
let inputContent = currentMessage.message 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) { if (result.length > 0 && _id !== chatMessage?._id) {
// Ouch we have got comment with same id created already. // Ouch we have got comment with same id created already.
currentMessage = getDefault() currentMessage = getDefault()
@ -108,17 +114,46 @@
async function createMessage (event: CustomEvent) { async function createMessage (event: CustomEvent) {
const { message, attachments } = event.detail const { message, attachments } = event.detail
await client.addCollection<Doc, ChatMessage>( if (_class === chunter.class.ThreadMessage) {
_class, const parentMessage = object as ActivityMessage
object.space,
object._id, await client.addCollection<ActivityMessage, ThreadMessage>(
object._class, chunter.class.ThreadMessage,
'comments', parentMessage.space,
{ message, attachments }, parentMessage._id,
_id, parentMessage._class,
Date.now(), 'replies',
account._id {
) 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) { async function editMessage (event: CustomEvent) {
@ -126,7 +161,7 @@
return return
} }
const { message, attachments } = event.detail 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 { export function submit (): void {
inputRef.submit() inputRef.submit()

View File

@ -24,10 +24,10 @@
import view from '@hcengineering/view' import view from '@hcengineering/view'
import activity, { DisplayActivityMessage } from '@hcengineering/activity' import activity, { DisplayActivityMessage } from '@hcengineering/activity'
import { ActivityMessageTemplate } from '@hcengineering/activity-resources' import { ActivityMessageTemplate } from '@hcengineering/activity-resources'
import chunter, { ChatMessage, ChatMessageViewlet } from '@hcengineering/chunter'
import ChatMessageHeader from './ChatMessageHeader.svelte' import ChatMessageHeader from './ChatMessageHeader.svelte'
import ChatMessageInput from './ChatMessageInput.svelte' import ChatMessageInput from './ChatMessageInput.svelte'
import chunter, { ChatMessage, ChatMessageViewlet } from '@hcengineering/chunter'
export let value: ChatMessage | undefined export let value: ChatMessage | undefined
export let showNotify: boolean = false export let showNotify: boolean = false
@ -35,8 +35,12 @@
export let isSelected: boolean = false export let isSelected: boolean = false
export let shouldScroll: boolean = false export let shouldScroll: boolean = false
export let embedded: 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 onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
@ -58,7 +62,7 @@
$: value && $: value &&
viewletQuery.query( viewletQuery.query(
chunter.class.ChatMessageViewlet, chunter.class.ChatMessageViewlet,
{ objectClass: value.attachedToClass }, { objectClass: value.attachedToClass, messageClass: value._class },
(result: ChatMessageViewlet[]) => { (result: ChatMessageViewlet[]) => {
viewlet = result[0] viewlet = result[0]
} }
@ -131,13 +135,14 @@
{ {
label: activity.string.Edit, label: activity.string.Edit,
icon: IconEdit, icon: IconEdit,
group: 'edit',
action: handleEditAction action: handleEditAction
} }
] ]
: []) : [])
] ]
$: excludedActions = isOwn ? [] : [chunter.action.DeleteChatMessage] $: excludedActions = []
let refInput: ChatMessageInput let refInput: ChatMessageInput
</script> </script>
@ -153,12 +158,15 @@
{isSelected} {isSelected}
{shouldScroll} {shouldScroll}
{embedded} {embedded}
{hasActionsMenu} {withActions}
{actions} {actions}
{showEmbedded}
{hideReplies}
{onClick} {onClick}
{onReply}
> >
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<ChatMessageHeader {object} {parentObject} message={value} {viewlet} {person} /> <ChatMessageHeader {object} {parentObject} message={value} {viewlet} {person} {skipLabel} />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="content"> <svelte:fragment slot="content">
{#if !isEditing} {#if !isEditing}

View 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>

View File

@ -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>

View File

@ -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"); // 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 // you may not use this file except in compliance with the License. You may
@ -15,36 +15,43 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { IconFolder, EditBox, ToggleWithLabel, Grid } from '@hcengineering/ui' import { IconFolder, EditBox, ToggleWithLabel, Grid } from '@hcengineering/ui'
import workbench from '@hcengineering/workbench'
import presentation, { getClient, SpaceCreateCard } from '@hcengineering/presentation' import presentation, { getClient, SpaceCreateCard } from '@hcengineering/presentation'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import chunter from '../plugin'
import core, { getCurrentAccount } from '@hcengineering/core' import core, { getCurrentAccount } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import chunter from '../../../plugin'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let isPrivate: boolean = false let isPrivate: boolean = false
let name: string = '' let name: string = ''
export function canClose (): boolean { export function canClose (): boolean {
return name === '' return name === ''
} }
const client = getClient() const client = getClient()
async function createChannel () { async function createChannel () {
const accountId = getCurrentAccount()._id
const channelId = await client.createDoc(chunter.class.Channel, core.space.Space, { const channelId = await client.createDoc(chunter.class.Channel, core.space.Space, {
name, name,
description: '', description: '',
private: isPrivate, private: isPrivate,
archived: false, 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, { const navigate = await getResource(chunter.actionImpl.OpenChannel)
mode: 'space',
space: channelId await navigate(undefined, undefined, { _id: notifyContextId })
})
} }
</script> </script>

View File

@ -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"); // 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 // you may not use this file except in compliance with the License. You may
@ -16,14 +16,15 @@
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { DirectMessage } from '@hcengineering/chunter'
import contact, { Employee } from '@hcengineering/contact' import contact, { Employee } from '@hcengineering/contact'
import core, { getCurrentAccount, Ref } from '@hcengineering/core' import core, { getCurrentAccount, Ref } from '@hcengineering/core'
import { getClient, SpaceCreateCard } from '@hcengineering/presentation' import { getClient, SpaceCreateCard } from '@hcengineering/presentation'
import workbench from '@hcengineering/workbench'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import chunter from '../plugin'
import { UserBoxList } from '@hcengineering/contact-resources' import { UserBoxList } from '@hcengineering/contact-resources'
import notification, { DocNotifyContext } from '@hcengineering/notification'
import chunter from '../../../plugin'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
@ -33,17 +34,26 @@
async function createDirectMessage () { async function createDirectMessage () {
const employeeAccounts = await client.findAll(contact.class.PersonAccount, { person: { $in: employeeIds } }) 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 existingContexts = await client.findAll<DocNotifyContext>(
const existingDms = await client.findAll(chunter.class.DirectMessage, {}) notification.class.DocNotifyContext,
const navigate = await getResource(workbench.actionImpl.Navigate) {
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 return
} }
} }
@ -56,10 +66,14 @@
members: accIds members: accIds
}) })
await navigate([], undefined as any, { const notifyContextId = await client.createDoc(notification.class.DocNotifyContext, core.space.Space, {
mode: 'space', user: myAccId,
space: dmId attachedTo: dmId,
attachedToClass: chunter.class.DirectMessage,
hidden: false
}) })
await navigate(undefined, undefined, { _id: notifyContextId })
} }
</script> </script>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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} />
&#183
{/if}
{space.members.length}
&#183
{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>

View File

@ -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"> <script lang="ts">
import attachment from '@hcengineering/attachment' import attachment from '@hcengineering/attachment'
import { FileBrowser } from '@hcengineering/attachment-resources' import { FileBrowser } from '@hcengineering/attachment-resources'
import { AnySvelteComponent, Button } from '@hcengineering/ui' import { AnySvelteComponent, Button } from '@hcengineering/ui'
import workbench from '@hcengineering/workbench' import workbench from '@hcengineering/workbench'
import { SpaceBrowser } from '@hcengineering/workbench-resources' import { SpaceBrowser } from '@hcengineering/workbench-resources'
import Header from './Header.svelte'
import contact from '@hcengineering/contact-resources/src/plugin' import contact from '@hcengineering/contact-resources/src/plugin'
import { EmployeeBrowser } from '@hcengineering/contact-resources' import { EmployeeBrowser } from '@hcengineering/contact-resources'
import { userSearch } from '../index'
import plugin from '../plugin'
import { SearchType } from '../utils'
import MessagesBrowser from './MessagesBrowser.svelte' import MessagesBrowser from './MessagesBrowser.svelte'
import { FilterButton } from '@hcengineering/view-resources' import { FilterButton } from '@hcengineering/view-resources'
import { Class, Doc, Ref } from '@hcengineering/core' 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 = '' let userSearch_: string = ''
userSearch.subscribe((v) => (userSearch_ = v)) userSearch.subscribe((v) => (userSearch_ = v))
@ -25,7 +40,7 @@
filterClass?: Ref<Class<Doc>> filterClass?: Ref<Class<Doc>>
props?: Record<string, any> props?: Record<string, any>
}[] = [ }[] = [
{ searchType: SearchType.Messages, component: MessagesBrowser, filterClass: plugin.class.ChunterMessage }, { searchType: SearchType.Messages, component: MessagesBrowser },
{ {
searchType: SearchType.Channels, searchType: SearchType.Channels,
component: SpaceBrowser, component: SpaceBrowser,

View File

@ -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}

View File

@ -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>

View File

@ -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>

View 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
}>
}

View 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