UBERF-5527: add context menu for activity and inbox (#5373)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-04-17 16:26:30 +04:00 committed by GitHub
parent 577c948d29
commit 7405b01d04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 421 additions and 419 deletions

View File

@ -21,8 +21,6 @@ import {
type ActivityInfoMessage, type ActivityInfoMessage,
type ActivityMessage, type ActivityMessage,
type ActivityMessageControl, type ActivityMessageControl,
type ActivityMessageExtension,
type ActivityMessageExtensionKind,
type ActivityMessagePreview, type ActivityMessagePreview,
type ActivityMessagesFilter, type ActivityMessagesFilter,
type ActivityReference, type ActivityReference,
@ -70,7 +68,7 @@ import {
} from '@hcengineering/model' } from '@hcengineering/model'
import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core' import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference' import preference, { TPreference } from '@hcengineering/model-preference'
import view from '@hcengineering/model-view' import view, { createAction } from '@hcengineering/model-view'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
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'
@ -210,15 +208,6 @@ export class TDocUpdateMessageViewlet extends TDoc implements DocUpdateMessageVi
onlyWithParent?: boolean onlyWithParent?: boolean
} }
@Model(activity.class.ActivityMessageExtension, core.class.Doc, DOMAIN_MODEL)
export class TActivityMessageExtension extends TDoc implements ActivityMessageExtension {
@Prop(TypeRef(activity.class.ActivityMessage), core.string.Class)
@Index(IndexKind.Indexed)
ofMessage!: Ref<Class<ActivityMessage>>
components!: { kind: ActivityMessageExtensionKind, component: AnyComponent }[]
}
@Model(activity.class.ActivityExtension, core.class.Doc, DOMAIN_MODEL) @Model(activity.class.ActivityExtension, core.class.Doc, DOMAIN_MODEL)
export class TActivityExtension extends TDoc implements ActivityExtension { export class TActivityExtension extends TDoc implements ActivityExtension {
@Prop(TypeRef(core.class.Class), core.string.Class) @Prop(TypeRef(core.class.Class), core.string.Class)
@ -275,7 +264,6 @@ export function createModel (builder: Builder): void {
TTxViewlet, TTxViewlet,
TActivityDoc, TActivityDoc,
TActivityMessagesFilter, TActivityMessagesFilter,
TActivityMessageExtension,
TActivityMessage, TActivityMessage,
TDocUpdateMessage, TDocUpdateMessage,
TDocUpdateMessageViewlet, TDocUpdateMessageViewlet,
@ -426,6 +414,113 @@ export function createModel (builder: Builder): void {
{ attachedToClass: 1 } { attachedToClass: 1 }
] ]
}) })
createAction(
builder,
{
action: activity.actionImpl.AddReaction,
label: activity.string.AddReaction,
icon: activity.icon.Emoji,
input: 'focus',
category: activity.category.Activity,
target: activity.class.ActivityMessage,
inline: true,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.AddReactionAction
)
createAction(
builder,
{
action: activity.actionImpl.SaveForLater,
label: activity.string.SaveForLater,
icon: activity.icon.Bookmark,
input: 'focus',
inline: true,
actionProps: {
size: 'x-small'
},
category: activity.category.Activity,
target: activity.class.ActivityMessage,
visibilityTester: activity.function.CanSaveForLater,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.SaveForLaterAction
)
createAction(
builder,
{
action: activity.actionImpl.RemoveFromSaved,
label: activity.string.RemoveFromLater,
icon: activity.icon.BookmarkFilled,
input: 'focus',
inline: true,
actionProps: {
iconProps: {
fill: 'var(--global-accent-TextColor)'
}
},
category: activity.category.Activity,
target: activity.class.ActivityMessage,
visibilityTester: activity.function.CanRemoveFromSaved,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.RemoveFromLaterAction
)
createAction(
builder,
{
action: activity.actionImpl.PinMessage,
label: view.string.Pin,
icon: view.icon.Pin,
input: 'focus',
inline: true,
category: activity.category.Activity,
target: activity.class.ActivityMessage,
visibilityTester: activity.function.CanPinMessage,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.PinMessageAction
)
createAction(
builder,
{
action: activity.actionImpl.UnpinMessage,
label: view.string.Unpin,
icon: view.icon.Pin,
input: 'focus',
inline: true,
actionProps: {
iconProps: {
fill: 'var(--global-accent-TextColor)'
}
},
category: activity.category.Activity,
target: activity.class.ActivityMessage,
visibilityTester: activity.function.CanUnpinMessage,
context: {
mode: 'context',
group: 'edit'
}
},
activity.ids.UnpinMessageAction
)
} }
export default activity export default activity

View File

@ -16,7 +16,13 @@ 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 ActionCategory } from '@hcengineering/view' import type { Location } from '@hcengineering/ui'
import {
type Action,
type ActionCategory,
type ViewAction,
type ViewActionAvailabilityFunction
} from '@hcengineering/view'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
export default mergeIds(activityId, activity, { export default mergeIds(activityId, activity, {
@ -24,7 +30,10 @@ export default mergeIds(activityId, activity, {
Attributes: '' as IntlString, Attributes: '' as IntlString,
Pinned: '' as IntlString, Pinned: '' as IntlString,
Emoji: '' as IntlString, Emoji: '' as IntlString,
Replies: '' as IntlString Replies: '' as IntlString,
AddReaction: '' as IntlString,
SaveForLater: '' as IntlString,
RemoveFromLater: '' as IntlString
}, },
filter: { filter: {
AttributesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>, AttributesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
@ -35,9 +44,28 @@ export default mergeIds(activityId, activity, {
ids: { ids: {
ReactionAddedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>, ReactionAddedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
ActivityNotificationGroup: '' as Ref<NotificationGroup>, ActivityNotificationGroup: '' as Ref<NotificationGroup>,
AddReactionNotification: '' as Ref<NotificationType> AddReactionNotification: '' as Ref<NotificationType>,
AddReactionAction: '' as Ref<Action>,
SaveForLaterAction: '' as Ref<Action>,
RemoveFromLaterAction: '' as Ref<Action>,
PinMessageAction: '' as Ref<Action>,
UnpinMessageAction: '' as Ref<Action>
},
function: {
GetFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
CanSaveForLater: '' as Resource<ViewActionAvailabilityFunction>,
CanRemoveFromSaved: '' as Resource<ViewActionAvailabilityFunction>,
CanPinMessage: '' as Resource<ViewActionAvailabilityFunction>,
CanUnpinMessage: '' as Resource<ViewActionAvailabilityFunction>
}, },
category: { category: {
Activity: '' as Ref<ActionCategory> Activity: '' as Ref<ActionCategory>
},
actionImpl: {
AddReaction: '' as ViewAction,
SaveForLater: '' as ViewAction,
RemoveFromSaved: '' as ViewAction,
PinMessage: '' as ViewAction,
UnpinMessage: '' as ViewAction
} }
}) })

View File

@ -677,46 +677,6 @@ export function createModel (builder: Builder, options = { addApplication: true
components: { input: chunter.component.ChatMessageInput } components: { input: chunter.component.ChatMessageInput }
}) })
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: chunter.class.ChatMessage,
components: [{ kind: 'footer', component: chunter.component.Replies }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: activity.class.DocUpdateMessage,
components: [{ kind: 'footer', component: chunter.component.Replies }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: activity.class.ActivityInfoMessage,
components: [{ kind: 'footer', component: chunter.component.Replies }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: activity.class.ActivityReference,
components: [{ kind: 'footer', component: chunter.component.Replies }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: chunter.class.ChatMessage,
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: activity.class.DocUpdateMessage,
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: activity.class.ActivityInfoMessage,
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
})
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
ofMessage: activity.class.ActivityReference,
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
})
builder.mixin(chunter.class.Channel, core.class.Class, chunter.mixin.ObjectChatPanel, { builder.mixin(chunter.class.Channel, core.class.Class, chunter.mixin.ObjectChatPanel, {
ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description'] ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description']
}) })
@ -741,6 +701,25 @@ export function createModel (builder: Builder, options = { addApplication: true
builder.createDoc(activity.class.ReplyProvider, core.space.Model, { builder.createDoc(activity.class.ReplyProvider, core.space.Model, {
function: chunter.function.ReplyToThread function: chunter.function.ReplyToThread
}) })
createAction(
builder,
{
action: chunter.actionImpl.ReplyToThread,
label: chunter.string.ReplyToThread,
icon: chunter.icon.Thread,
input: 'focus',
category: chunter.category.Chunter,
target: activity.class.ActivityMessage,
visibilityTester: chunter.function.CanReplyToThread,
inline: true,
context: {
mode: 'context',
group: 'edit'
}
},
chunter.action.ReplyToThreadAction
)
} }
export default chunter export default chunter

View File

@ -40,13 +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>,
CopyChatMessageLink: '' as Ref<Action<Doc, any>> CopyChatMessageLink: '' as Ref<Action<Doc, any>>,
ReplyToThreadAction: '' as Ref<Action>
}, },
actionImpl: { actionImpl: {
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>
@ -100,8 +102,9 @@ export default mergeIds(chunterId, chunter, {
CanCopyMessageLink: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>, CanCopyMessageLink: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
GetChunterSpaceLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>, GetChunterSpaceLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
GetThreadLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>, GetThreadLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
GetMessageLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>, ReplyToThread: '' as Resource<(doc: ActivityMessage) => Promise<void>>,
ReplyToThread: '' as Resource<(doc: ActivityMessage) => Promise<void>> CanReplyToThread: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
GetMessageLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
}, },
filter: { filter: {
ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean> ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>

View File

@ -111,6 +111,7 @@ export function closePopup (category?: string): void {
} else { } else {
for (let i = popups.length - 1; i >= 0; i--) { for (let i = popups.length - 1; i >= 0; i--) {
if (popups[i].options.fixed !== true) { if (popups[i].options.fixed !== true) {
popups[i].onClose?.(undefined)
popups.splice(i, 1) popups.splice(i, 1)
break break
} }

View File

@ -239,6 +239,7 @@ export type IconSize =
| 'inline' | 'inline'
| 'tiny' | 'tiny'
| 'card' | 'card'
| 'xx-small'
| 'x-small' | 'x-small'
| 'smaller' | 'smaller'
| 'small' | 'small'
@ -258,6 +259,7 @@ export function getIconSize2x (size: IconSize): IconSize {
switch (size) { switch (size) {
case 'inline': case 'inline':
case 'tiny': case 'tiny':
case 'xx-small':
case 'x-small': case 'x-small':
case 'small': case 'small':
case 'card': case 'card':

View File

@ -11,4 +11,7 @@
<symbol id="bookmark" viewBox="0 0 20 28"> <symbol id="bookmark" viewBox="0 0 20 28">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 4.00012C0 1.79098 1.79086 0.00012207 4 0.00012207H16C18.2091 0.00012207 20 1.79098 20 4.00012V27.0001C20 27.3689 19.797 27.7078 19.4719 27.8818C19.1467 28.0558 18.7522 28.0367 18.4453 27.8322L10 22.202L1.5547 27.8322C1.24784 28.0367 0.8533 28.0558 0.528142 27.8818C0.202985 27.7078 0 27.3689 0 27.0001V4.00012ZM4 2.00012C2.89543 2.00012 2 2.89555 2 4.00012V25.1316L9.4453 20.1681C9.7812 19.9441 10.2188 19.9441 10.5547 20.1681L18 25.1316V4.00012C18 2.89555 17.1046 2.00012 16 2.00012H4Z" /> <path fill-rule="evenodd" clip-rule="evenodd" d="M0 4.00012C0 1.79098 1.79086 0.00012207 4 0.00012207H16C18.2091 0.00012207 20 1.79098 20 4.00012V27.0001C20 27.3689 19.797 27.7078 19.4719 27.8818C19.1467 28.0558 18.7522 28.0367 18.4453 27.8322L10 22.202L1.5547 27.8322C1.24784 28.0367 0.8533 28.0558 0.528142 27.8818C0.202985 27.7078 0 27.3689 0 27.0001V4.00012ZM4 2.00012C2.89543 2.00012 2 2.89555 2 4.00012V25.1316L9.4453 20.1681C9.7812 19.9441 10.2188 19.9441 10.5547 20.1681L18 25.1316V4.00012C18 2.89555 17.1046 2.00012 16 2.00012H4Z" />
</symbol> </symbol>
<symbol id="bookmark-filled" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 6.00012C6 3.79098 7.79086 2.00012 10 2.00012H22C24.2091 2.00012 26 3.79098 26 6.00012V29.0001C26 29.3689 25.797 29.7078 25.4719 29.8818C25.1467 30.0558 24.7522 30.0367 24.4453 29.8322L16 24.202L7.5547 29.8322C7.24784 30.0367 6.8533 30.0558 6.52814 29.8818C6.20298 29.7078 6 29.3689 6 29.0001V6.00012Z" />
</symbol>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -40,6 +40,9 @@
"Mentions": "Mentions", "Mentions": "Mentions",
"MentionedYouIn": "Mentioned you in {title}", "MentionedYouIn": "Mentioned you in {title}",
"Messages": "Messages", "Messages": "Messages",
"Thread": "Thread" "Thread": "Thread",
"AddReaction": "Add reaction",
"SaveForLater": "Save for later",
"RemoveFromLater": "Remove from saved"
} }
} }

View File

@ -40,6 +40,9 @@
"Mentions": "Упоминания", "Mentions": "Упоминания",
"MentionedYouIn": "Упомянул(а) вас в {title}", "MentionedYouIn": "Упомянул(а) вас в {title}",
"Messages": "Cообщения", "Messages": "Cообщения",
"Thread": "Обсуждение" "Thread": "Обсуждение",
"AddReaction": "Добавить реакцию",
"SaveForLater": "Cохранить",
"RemoveFromLater": "Удалить из сохраненных"
} }
} }

View File

@ -20,5 +20,6 @@ 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` Bookmark: `${icons}#bookmark`,
BookmarkFilled: `${icons}#bookmark-filled`
}) })

View File

@ -14,9 +14,10 @@
--> -->
<script lang="ts"> <script lang="ts">
import { type AnySvelteComponent, ButtonIcon, IconSize } from '@hcengineering/ui' import { type AnySvelteComponent, ButtonIcon, IconSize } from '@hcengineering/ui'
import { Asset } from '@hcengineering/platform' import { Asset, IntlString } from '@hcengineering/platform'
import { ComponentType } from 'svelte' import { ComponentType } from 'svelte'
export let label: IntlString
export let icon: Asset | AnySvelteComponent | ComponentType export let icon: Asset | AnySvelteComponent | ComponentType
export let iconProps: any | undefined = undefined export let iconProps: any | undefined = undefined
export let size: IconSize = 'small' export let size: IconSize = 'small'
@ -30,4 +31,13 @@
} }
</script> </script>
<ButtonIcon {icon} {iconProps} iconSize={size} size="small" kind="tertiary" pressed={opened} on:click={onClick} /> <ButtonIcon
{icon}
{iconProps}
iconSize={size}
size="small"
kind="tertiary"
pressed={opened}
on:click={onClick}
tooltip={{ label }}
/>

View File

@ -13,60 +13,95 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import activity, { ActivityMessage, ActivityMessageExtension } from '@hcengineering/activity' import activity, { ActivityMessage } from '@hcengineering/activity'
import { Action, IconMoreV, showPopup } from '@hcengineering/ui' import { Action, IconMoreV, showPopup } from '@hcengineering/ui'
import { Menu } from '@hcengineering/view-resources' import { getActions, Menu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { getClient } from '@hcengineering/presentation'
import { getResource } from '@hcengineering/platform'
import view, { Action as ViewAction } from '@hcengineering/view'
import ActivityMessageAction from './ActivityMessageAction.svelte' import ActivityMessageAction from './ActivityMessageAction.svelte'
import PinMessageAction from './PinMessageAction.svelte' import { savedMessagesStore } from '../activity'
import SaveMessageAction from './SaveMessageAction.svelte'
import ActivityMessageExtensionComponent from './activity-message/ActivityMessageExtension.svelte'
import AddReactionAction from './reactions/AddReactionAction.svelte'
export let message: ActivityMessage | undefined export let message: ActivityMessage | undefined
export let extensions: ActivityMessageExtension[] = []
export let actions: Action[] = [] export let actions: Action[] = []
export let withActionMenu = true export let withActionMenu = true
export let onOpen: () => void
export let onClose: () => void
const dispatch = createEventDispatcher() const client = getClient()
let inlineActions: ViewAction[] = []
let isActionMenuOpened = false let isActionMenuOpened = false
$: void updateInlineActions(message)
savedMessagesStore.subscribe(() => {
void updateInlineActions(message)
})
function handleActionMenuOpened (): void { function handleActionMenuOpened (): void {
isActionMenuOpened = true isActionMenuOpened = true
dispatch('open') onOpen()
} }
function handleActionMenuClosed (): void { function handleActionMenuClosed (): void {
isActionMenuOpened = false isActionMenuOpened = false
dispatch('close') onClose()
} }
function showMenu (ev: MouseEvent): void { function showMenu (ev: MouseEvent): void {
const excludedActions = inlineActions.map(({ _id }) => _id)
showPopup( showPopup(
Menu, Menu,
{ {
object: message, object: message,
actions, actions,
baseMenuClass: activity.class.ActivityMessage baseMenuClass: activity.class.ActivityMessage,
excludedActions
}, },
ev.target as HTMLElement, ev.target as HTMLElement,
handleActionMenuClosed handleActionMenuClosed
) )
handleActionMenuOpened() handleActionMenuOpened()
} }
async function updateInlineActions (message?: ActivityMessage): Promise<void> {
if (message === undefined) {
inlineActions = []
return
}
inlineActions = (await getActions(client, message, activity.class.ActivityMessage)).filter(
(action) => action.inline
)
}
</script> </script>
{#if message} {#if message}
<div class="root"> <div class="root">
<AddReactionAction object={message} on:open on:close /> {#each inlineActions as inline}
<ActivityMessageExtensionComponent kind="action" {extensions} props={{ object: message }} on:close on:open /> {#if inline.icon}
<PinMessageAction object={message} /> {#await getResource(inline.action) then action}
<SaveMessageAction object={message} /> <ActivityMessageAction
label={inline.label}
size={inline.actionProps?.size ?? 'small'}
icon={inline.icon}
iconProps={inline.actionProps?.iconProps}
action={(ev) => action(message, ev, { onOpen, onClose })}
/>
{/await}
{/if}
{/each}
{#if withActionMenu} {#if withActionMenu}
<ActivityMessageAction size="small" icon={IconMoreV} opened={isActionMenuOpened} action={showMenu} /> <ActivityMessageAction
size="small"
icon={IconMoreV}
opened={isActionMenuOpened}
action={showMenu}
label={view.string.MoreActions}
/>
{/if} {/if}
</div> </div>
{/if} {/if}

View File

@ -26,9 +26,10 @@
import core, { Account, Doc, Ref, Timestamp } from '@hcengineering/core' import core, { Account, Doc, Ref, Timestamp } from '@hcengineering/core'
import { Icon, Label, resizeObserver, TimeSince, tooltip } from '@hcengineering/ui' import { Icon, Label, resizeObserver, TimeSince, tooltip } from '@hcengineering/ui'
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform' import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import activity, { ActivityMessagePreviewType } from '@hcengineering/activity' import activity, { ActivityMessage, ActivityMessagePreviewType } from '@hcengineering/activity'
import { classIcon, DocNavLink } from '@hcengineering/view-resources' import { classIcon, DocNavLink, showMenu } from '@hcengineering/view-resources'
export let message: ActivityMessage | undefined = undefined
export let text: string | undefined = undefined export let text: string | undefined = undefined
export let intlLabel: IntlString | undefined = undefined export let intlLabel: IntlString | undefined = undefined
export let readonly = false export let readonly = false
@ -99,6 +100,12 @@
width = element.clientWidth width = element.clientWidth
}} }}
on:click on:click
on:contextmenu={(evt) => {
showMenu(evt, { object: message, baseMenuClass: activity.class.ActivityMessage }, () => {
isActionsOpened = false
})
isActionsOpened = true
}}
> >
<span class="left overflow-label"> <span class="left overflow-label">
{#if type === 'full'} {#if type === 'full'}

View File

@ -1,34 +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 { getClient } from '@hcengineering/presentation'
import view from '@hcengineering/view'
import { ActivityMessage } from '@hcengineering/activity'
import ActivityMessageAction from './ActivityMessageAction.svelte'
export let object: ActivityMessage
const client = getClient()
async function toggleMessagePinning (): Promise<void> {
await client.update(object, { isPinned: !object.isPinned })
}
</script>
<ActivityMessageAction
icon={view.icon.Pin}
iconProps={{ fill: object.isPinned ? '#3265cb' : 'currentColor' }}
action={toggleMessagePinning}
/>

View File

@ -29,7 +29,6 @@
export let object: ActivityMessage export let object: ActivityMessage
export let embedded = false export let embedded = false
export let onReply: (() => void) | undefined = undefined
const client = getClient() const client = getClient()
const maxDisplayPersons = 5 const maxDisplayPersons = 5
@ -83,11 +82,6 @@
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
if (onReply) {
onReply()
return
}
if (replyProvider) { if (replyProvider) {
const fn = await getResource(replyProvider.function) const fn = await getResource(replyProvider.function)
fn(object) fn(object)

View File

@ -1,52 +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 { createQuery, getClient } from '@hcengineering/presentation'
import activity, { ActivityMessage, SavedMessage } from '@hcengineering/activity'
import preference from '@hcengineering/preference'
import BookmarkBorder from './icons/BookmarkBorder.svelte'
import ActivityMessageAction from './ActivityMessageAction.svelte'
import Bookmark from './icons/Bookmark.svelte'
import { savedMessagesStore } from '../activity'
export let object: ActivityMessage
const client = getClient()
let savedMessage: SavedMessage | undefined = undefined
savedMessagesStore.subscribe((saved) => {
savedMessage = saved.find(({ attachedTo }) => attachedTo === object._id)
})
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>
<ActivityMessageAction
icon={savedMessage ? Bookmark : BookmarkBorder}
size={savedMessage ? 'small' : 'x-small'}
iconProps={{ fill: savedMessage ? 'var(--global-accent-TextColor)' : 'currentColor' }}
action={toggleSaveMessage}
/>

View File

@ -1,30 +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 { ActivityMessageExtension, ActivityMessageExtensionKind } from '@hcengineering/activity'
import { Component } from '@hcengineering/ui'
export let kind: ActivityMessageExtensionKind
export let extensions: ActivityMessageExtension[] = []
export let props: Record<string, any> = {}
</script>
{#each extensions as extension}
{#each extension.components as component}
{#if component.kind === kind}
<Component is={component.component} {props} showLoading={false} on:close on:open />
{/if}
{/each}
{/each}

View File

@ -39,7 +39,6 @@
export let hideLink = false export let hideLink = false
export let compact = false export let compact = 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()
@ -70,8 +69,7 @@
videoPreload, videoPreload,
hideLink, hideLink,
compact, compact,
onClick, onClick
onReply
}} }}
/> />
{/if} {/if}

View File

@ -13,23 +13,18 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import activity, { import activity, { ActivityMessageViewlet, DisplayActivityMessage } from '@hcengineering/activity'
ActivityMessageExtension,
ActivityMessageViewlet,
DisplayActivityMessage
} from '@hcengineering/activity'
import { Person } from '@hcengineering/contact' import { Person } from '@hcengineering/contact'
import { Avatar, EmployeePresenter, SystemAvatar } from '@hcengineering/contact-resources' import { Avatar, EmployeePresenter, SystemAvatar } from '@hcengineering/contact-resources'
import core from '@hcengineering/core' import core from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Action, Label } from '@hcengineering/ui' import { Action, Icon, Label } from '@hcengineering/ui'
import { getActions, restrictionStore } from '@hcengineering/view-resources' import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte' import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte' import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
import ActivityMessageActions from '../ActivityMessageActions.svelte' import ActivityMessageActions from '../ActivityMessageActions.svelte'
import { isReactionMessage } from '../../activityMessagesUtils' import { isReactionMessage } from '../../activityMessagesUtils'
import Bookmark from '../icons/Bookmark.svelte'
import { savedMessagesStore } from '../../activity' import { savedMessagesStore } from '../../activity'
import MessageTimestamp from '../MessageTimestamp.svelte' import MessageTimestamp from '../MessageTimestamp.svelte'
import Replies from '../Replies.svelte' import Replies from '../Replies.svelte'
@ -53,14 +48,12 @@
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover' export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let showDatePreposition = false export let showDatePreposition = 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 menuActionIds: string[] = [] let menuActionIds: string[] = []
let element: HTMLDivElement | undefined = undefined let element: HTMLDivElement | undefined = undefined
let extensions: ActivityMessageExtension[] = []
let isActionsOpened = false let isActionsOpened = false
let isSaved = false let isSaved = false
@ -85,8 +78,6 @@
setTimeout(scrollToMessage, 100) setTimeout(scrollToMessage, 100)
} }
$: extensions = client.getModel().findAllSync(activity.class.ActivityMessageExtension, { ofMessage: message._class })
function handleActionsOpened (): void { function handleActionsOpened (): void {
isActionsOpened = true isActionsOpened = true
} }
@ -121,6 +112,12 @@
class:borderedHover={hoverStyles === 'borderedHover'} class:borderedHover={hoverStyles === 'borderedHover'}
class:filledHover={hoverStyles === 'filledHover'} class:filledHover={hoverStyles === 'filledHover'}
on:click={onClick} on:click={onClick}
on:contextmenu={(evt) => {
showMenu(evt, { object: message, baseMenuClass: activity.class.ActivityMessage }, () => {
isActionsOpened = false
})
isActionsOpened = true
}}
> >
{#if showNotify && !embedded} {#if showNotify && !embedded}
<div class="notify" /> <div class="notify" />
@ -136,7 +133,7 @@
{/if} {/if}
{#if isSaved} {#if isSaved}
<div class="saveMarker"> <div class="saveMarker">
<Bookmark size="xx-small" /> <Icon icon={activity.icon.BookmarkFilled} size="xx-small" />
</div> </div>
{/if} {/if}
</div> </div>
@ -171,7 +168,7 @@
<slot name="content" /> <slot name="content" />
{#if !hideFooter} {#if !hideFooter}
<Replies {embedded} object={message} {onReply} /> <Replies {embedded} object={message} />
{/if} {/if}
<ReactionsPresenter object={message} {readonly} /> <ReactionsPresenter object={message} {readonly} />
{#if parentMessage && showEmbedded} {#if parentMessage && showEmbedded}
@ -184,11 +181,10 @@
<div class="actions" class:opened={isActionsOpened}> <div class="actions" class:opened={isActionsOpened}>
<ActivityMessageActions <ActivityMessageActions
message={isReactionMessage(message) ? parentMessage : message} message={isReactionMessage(message) ? parentMessage : message}
{extensions}
{actions} {actions}
{withActionMenu} {withActionMenu}
on:open={handleActionsOpened} onOpen={handleActionsOpened}
on:close={handleActionsClosed} onClose={handleActionsClosed}
/> />
</div> </div>
{/if} {/if}

View File

@ -14,7 +14,6 @@
--> -->
<script lang="ts"> <script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import activity, { ActivityMessage, ActivityMessagePreviewType } from '@hcengineering/activity' import activity, { ActivityMessage, ActivityMessagePreviewType } from '@hcengineering/activity'
@ -30,17 +29,14 @@
export let message: ActivityMessage export let message: ActivityMessage
export let actions: Action[] = [] export let actions: Action[] = []
const client = getClient()
let previewElement: BasePreview let previewElement: BasePreview
let isCompact = false let isCompact = false
$: extensions = client.getModel().findAllSync(activity.class.ActivityMessageExtension, { ofMessage: message._class })
</script> </script>
<BasePreview <BasePreview
bind:this={previewElement} bind:this={previewElement}
bind:isCompact bind:isCompact
{message}
{text} {text}
{intlLabel} {intlLabel}
{readonly} {readonly}
@ -58,13 +54,14 @@
{/if} {/if}
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="actions"> <svelte:fragment slot="actions">
{#if previewElement}
<ActivityMessageActions <ActivityMessageActions
{message} {message}
{extensions}
{actions} {actions}
withActionMenu={false} withActionMenu={false}
on:open={previewElement.onActionsOpened} onOpen={previewElement.onActionsOpened}
on:close={previewElement.onActionsClosed} onClose={previewElement.onActionsClosed}
/> />
{/if}
</svelte:fragment> </svelte:fragment>
</BasePreview> </BasePreview>

View File

@ -43,7 +43,6 @@
export let hideLink = false export let hideLink = false
export let compact = false export let compact = 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()
@ -112,7 +111,6 @@
{hoverStyles} {hoverStyles}
showDatePreposition showDatePreposition
{onClick} {onClick}
{onReply}
> >
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<span class="header"> <span class="header">

View File

@ -51,7 +51,6 @@
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover' export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let hideLink = false export let hideLink = 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()
@ -172,7 +171,6 @@
{hoverStyles} {hoverStyles}
showDatePreposition={hideLink} showDatePreposition={hideLink}
{onClick} {onClick}
{onReply}
> >
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<DocUpdateMessageHeader <DocUpdateMessageHeader

View File

@ -1,26 +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">
export let size: 'tiny' | 'xx-small' | 'x-small' | 'small' | 'medium' | 'large' = 'small'
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6 6.00012C6 3.79098 7.79086 2.00012 10 2.00012H22C24.2091 2.00012 26 3.79098 26 6.00012V29.0001C26 29.3689 25.797 29.7078 25.4719 29.8818C25.1467 30.0558 24.7522 30.0367 24.4453 29.8322L16 24.202L7.5547 29.8322C7.24784 30.0367 6.8533 30.0558 6.52814 29.8818C6.20298 29.7078 6 29.3689 6 29.0001V6.00012Z"
/>
</svg>

View File

@ -1,26 +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">
export let size: 'small' | 'medium' | 'large'
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 20 28" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 4.00012C0 1.79098 1.79086 0.00012207 4 0.00012207H16C18.2091 0.00012207 20 1.79098 20 4.00012V27.0001C20 27.3689 19.797 27.7078 19.4719 27.8818C19.1467 28.0558 18.7522 28.0367 18.4453 27.8322L10 22.202L1.5547 27.8322C1.24784 28.0367 0.8533 28.0558 0.528142 27.8818C0.202985 27.7078 0 27.3689 0 27.0001V4.00012ZM4 2.00012C2.89543 2.00012 2 2.89555 2 4.00012V25.1316L9.4453 20.1681C9.7812 19.9441 10.2188 19.9441 10.5547 20.1681L18 25.1316V4.00012C18 2.89555 17.1046 2.00012 16 2.00012H4Z"
/>
</svg>

View File

@ -1,52 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { EmojiPopup, showPopup } from '@hcengineering/ui'
import { createQuery, getClient } from '@hcengineering/presentation'
import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
import { updateDocReactions } from '../../utils'
import ActivityMessageAction from '../ActivityMessageAction.svelte'
export let object: ActivityMessage | undefined = undefined
const client = getClient()
const reactionsQuery = createQuery()
const dispatch = createEventDispatcher()
let reactions: Reaction[] = []
let isOpened = false
$: if (object?.reactions && object.reactions > 0) {
reactionsQuery.query(activity.class.Reaction, { attachedTo: object._id }, (res?: Reaction[]) => {
reactions = res || []
})
}
function openEmojiPalette (ev: Event) {
dispatch('open')
showPopup(EmojiPopup, {}, ev.target as HTMLElement, (emoji: string) => {
updateDocReactions(client, reactions, object, emoji)
isOpened = false
dispatch('close')
})
isOpened = true
}
</script>
<ActivityMessageAction icon={activity.icon.Emoji} action={openEmojiPalette} opened={isOpened} />

View File

@ -28,6 +28,17 @@ import ActivityInfoMessagePreview from './components/activity-info-message/Activ
import { attributesFilter, pinnedFilter, allFilter, referencesFilter } from './activityMessagesUtils' import { attributesFilter, pinnedFilter, allFilter, referencesFilter } from './activityMessagesUtils'
import { updateReferences } from './references' import { updateReferences } from './references'
import {
addReactionAction,
canPinMessage,
canRemoveFromSaved,
saveForLater,
unpinMessage,
pinMessage,
canSaveForLater,
canUnpinMessage,
removeFromSaved
} from './utils'
export * from './activity' export * from './activity'
export * from './utils' export * from './utils'
@ -42,7 +53,6 @@ export { default as ActivityDocLink } from './components/ActivityDocLink.svelte'
export { default as ReactionPresenter } from './components/reactions/ReactionPresenter.svelte' export { default as ReactionPresenter } from './components/reactions/ReactionPresenter.svelte'
export { default as ActivityMessageNotificationLabel } from './components/activity-message/ActivityMessageNotificationLabel.svelte' export { default as ActivityMessageNotificationLabel } from './components/activity-message/ActivityMessageNotificationLabel.svelte'
export { default as ActivityMessageHeader } from './components/activity-message/ActivityMessageHeader.svelte' export { default as ActivityMessageHeader } from './components/activity-message/ActivityMessageHeader.svelte'
export { default as AddReactionAction } from './components/reactions/AddReactionAction.svelte'
export { default as ActivityMessageAction } from './components/ActivityMessageAction.svelte' export { default as ActivityMessageAction } from './components/ActivityMessageAction.svelte'
export { default as ActivityMessagesFilterPopup } from './components/FilterPopup.svelte' export { default as ActivityMessagesFilterPopup } from './components/FilterPopup.svelte'
export { default as ActivityReferencePresenter } from './components/activity-reference/ActivityReferencePresenter.svelte' export { default as ActivityReferencePresenter } from './components/activity-reference/ActivityReferencePresenter.svelte'
@ -70,7 +80,20 @@ export default async (): Promise<Resources> => ({
AllFilter: allFilter, AllFilter: allFilter,
ReferencesFilter: referencesFilter ReferencesFilter: referencesFilter
}, },
function: {
CanSaveForLater: canSaveForLater,
CanRemoveFromSaved: canRemoveFromSaved,
CanPinMessage: canPinMessage,
CanUnpinMessage: canUnpinMessage
},
backreference: { backreference: {
Update: updateReferences Update: updateReferences
},
actionImpl: {
AddReaction: addReactionAction,
SaveForLater: saveForLater,
RemoveFromSaved: removeFromSaved,
PinMessage: pinMessage,
UnpinMessage: unpinMessage
} }
}) })

View File

@ -20,18 +20,23 @@ import core, {
getCurrentAccount getCurrentAccount
} from '@hcengineering/core' } from '@hcengineering/core'
import { type Asset, type IntlString, getResource, translate } from '@hcengineering/platform' import { type Asset, type IntlString, getResource, translate } from '@hcengineering/platform'
import { getAttributePresenterClass } from '@hcengineering/presentation' import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { import {
type AnyComponent, type AnyComponent,
type AnySvelteComponent, type AnySvelteComponent,
ErrorPresenter, ErrorPresenter,
themeStore, themeStore,
type Location type Location,
getEventPositionElement,
closePopup,
showPopup,
EmojiPopup
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view, { type AttributeModel, type BuildModelKey, type BuildModelOptions } from '@hcengineering/view' import view, { type AttributeModel, type BuildModelKey, type BuildModelOptions } from '@hcengineering/view'
import { getObjectPresenter } from '@hcengineering/view-resources' import { getObjectPresenter } from '@hcengineering/view-resources'
import preference from '@hcengineering/preference'
import { type ActivityKey, activityKey } from './activity' import { type ActivityKey, activityKey, savedMessagesStore } from './activity'
import activity from './plugin' import activity from './plugin'
const valueTypes: ReadonlyArray<Ref<Class<Doc>>> = [ const valueTypes: ReadonlyArray<Ref<Class<Doc>>> = [
@ -427,3 +432,90 @@ export async function updateDocReactions (
export function getMessageFromLoc (loc: Location): Ref<ActivityMessage> | undefined { export function getMessageFromLoc (loc: Location): Ref<ActivityMessage> | undefined {
return (loc.query?.message ?? undefined) as Ref<ActivityMessage> | undefined return (loc.query?.message ?? undefined) as Ref<ActivityMessage> | undefined
} }
interface ActivityMessageActionParams {
onClose?: () => void
onOpen?: () => void
}
export async function addReactionAction (
message?: ActivityMessage,
ev?: MouseEvent,
params?: ActivityMessageActionParams
): Promise<void> {
if (message === undefined || ev === undefined) return
const client = getClient()
const reactions: Reaction[] =
(message.reactions ?? 0) > 0
? await client.findAll<Reaction>(activity.class.Reaction, { attachedTo: message._id })
: []
const element = getEventPositionElement(ev)
closePopup()
showPopup(EmojiPopup, {}, element, (emoji: string) => {
void updateDocReactions(client, reactions, message, emoji)
params?.onClose?.()
})
params?.onOpen?.()
}
export async function saveForLater (message?: ActivityMessage): Promise<void> {
if (message === undefined) return
closePopup()
const client = getClient()
await client.createDoc(activity.class.SavedMessage, preference.space.Preference, {
attachedTo: message._id
})
}
export async function removeFromSaved (message?: ActivityMessage): Promise<void> {
if (message === undefined) return
closePopup()
const client = getClient()
const saved = get(savedMessagesStore).find((saved) => saved.attachedTo === message._id)
if (saved !== undefined) {
await client.removeDoc(saved._class, saved.space, saved._id)
}
}
export async function canSaveForLater (message?: ActivityMessage): Promise<boolean> {
if (message === undefined) return false
const saved = get(savedMessagesStore).find((saved) => saved.attachedTo === message._id)
return saved === undefined
}
export async function canRemoveFromSaved (message?: ActivityMessage): Promise<boolean> {
if (message === undefined) return false
return !(await canSaveForLater(message))
}
export async function canPinMessage (message?: ActivityMessage): Promise<boolean> {
return message !== undefined && message.isPinned !== true
}
export async function canUnpinMessage (message?: ActivityMessage): Promise<boolean> {
return message !== undefined && message.isPinned === true
}
export async function pinMessage (message?: ActivityMessage): Promise<void> {
if (message === undefined) return
closePopup()
const client = getClient()
await client.update(message, { isPinned: true })
}
export async function unpinMessage (message?: ActivityMessage): Promise<void> {
if (message === undefined) return
closePopup()
const client = getClient()
await client.update(message, { isPinned: false })
}

View File

@ -157,16 +157,6 @@ export interface ActivityInfoMessage extends ActivityMessage {
links?: { _class: Ref<Class<Doc>>, _id: Ref<Doc> }[] links?: { _class: Ref<Class<Doc>>, _id: Ref<Doc> }[]
} }
export type ActivityMessageExtensionKind = 'action' | 'footer'
/**
* @public
*/
export interface ActivityMessageExtension extends Doc {
ofMessage: Ref<Class<ActivityMessage>>
components: { kind: ActivityMessageExtensionKind, component: AnyComponent }[]
}
/** /**
* @public * @public
*/ */
@ -333,7 +323,6 @@ export default plugin(activityId, {
ActivityInfoMessage: '' as Ref<Class<ActivityInfoMessage>>, ActivityInfoMessage: '' as Ref<Class<ActivityInfoMessage>>,
ActivityMessageControl: '' as Ref<Class<ActivityMessageControl>>, ActivityMessageControl: '' as Ref<Class<ActivityMessageControl>>,
DocUpdateMessageViewlet: '' as Ref<Class<DocUpdateMessageViewlet>>, DocUpdateMessageViewlet: '' as Ref<Class<DocUpdateMessageViewlet>>,
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>>,
@ -344,7 +333,8 @@ export default plugin(activityId, {
icon: { icon: {
Activity: '' as Asset, Activity: '' as Asset,
Emoji: '' as Asset, Emoji: '' as Asset,
Bookmark: '' as Asset Bookmark: '' as Asset,
BookmarkFilled: '' as Asset
}, },
string: { string: {
Activity: '' as IntlString, Activity: '' as IntlString,

View File

@ -1,32 +0,0 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ActivityMessage } from '@hcengineering/activity'
import { ActivityMessageAction } from '@hcengineering/activity-resources'
import chunter from './../plugin'
import { replyToThread } from '../index'
import { canReplyToThread } from '../utils'
export let object: ActivityMessage
function onReply (): void {
replyToThread(object)
}
</script>
{#if canReplyToThread(object)}
<ActivityMessageAction size="small" icon={chunter.icon.Thread} action={onReply} />
{/if}

View File

@ -50,7 +50,6 @@
export let hideLink = false export let hideLink = false
export let compact = false export let compact = 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()
@ -183,7 +182,6 @@
{skipLabel} {skipLabel}
showDatePreposition={hideLink} showDatePreposition={hideLink}
{onClick} {onClick}
{onReply}
> >
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<ChatMessageHeader {object} {parentObject} message={value} {viewlet} {person} {skipLabel} {hideLink} /> <ChatMessageHeader {object} {parentObject} message={value} {viewlet} {person} {skipLabel} {hideLink} />

View File

@ -40,7 +40,6 @@
export let attachmentImageSize: AttachmentImageSize = 'x-large' export let attachmentImageSize: AttachmentImageSize = 'x-large'
export let videoPreload = true export let videoPreload = true
export let onClick: (() => void) | undefined = undefined export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient() const client = getClient()
</script> </script>
@ -76,6 +75,5 @@
{videoPreload} {videoPreload}
showLinksPreview={false} showLinksPreview={false}
{onClick} {onClick}
{onReply}
/> />
{/if} {/if}

View File

@ -48,7 +48,6 @@ import ChannelIcon from './components/ChannelIcon.svelte'
import ThreadNotificationPresenter from './components/notification/ThreadNotificationPresenter.svelte' import ThreadNotificationPresenter from './components/notification/ThreadNotificationPresenter.svelte'
import ChatMessageNotificationLabel from './components/notification/ChatMessageNotificationLabel.svelte' import ChatMessageNotificationLabel from './components/notification/ChatMessageNotificationLabel.svelte'
import ChatAside from './components/chat/ChatAside.svelte' import ChatAside from './components/chat/ChatAside.svelte'
import ReplyToThreadAction from './components/ReplyToThreadAction.svelte'
import ThreadMessagePreview from './components/threads/ThreadMessagePreview.svelte' import ThreadMessagePreview from './components/threads/ThreadMessagePreview.svelte'
import ChatMessagePreview from './components/chat-message/ChatMessagePreview.svelte' import ChatMessagePreview from './components/chat-message/ChatMessagePreview.svelte'
@ -62,7 +61,8 @@ import {
getUnreadThreadsCount, getUnreadThreadsCount,
canCopyMessageLink, canCopyMessageLink,
leaveChannelAction, leaveChannelAction,
removeChannelAction removeChannelAction,
canReplyToThread
} from './utils' } from './utils'
import { import {
chunterSpaceLinkFragmentProvider, chunterSpaceLinkFragmentProvider,
@ -180,7 +180,6 @@ export default async (): Promise<Resources> => ({
ChatMessageNotificationLabel, ChatMessageNotificationLabel,
ThreadNotificationPresenter, ThreadNotificationPresenter,
ChatAside, ChatAside,
ReplyToThreadAction,
ThreadMessagePreview, ThreadMessagePreview,
ChatMessagePreview ChatMessagePreview
}, },
@ -197,8 +196,9 @@ export default async (): Promise<Resources> => ({
GetChunterSpaceLinkFragment: chunterSpaceLinkFragmentProvider, GetChunterSpaceLinkFragment: chunterSpaceLinkFragmentProvider,
GetUnreadThreadsCount: getUnreadThreadsCount, GetUnreadThreadsCount: getUnreadThreadsCount,
GetThreadLink: getThreadLink, GetThreadLink: getThreadLink,
GetMessageLink: getMessageLocation, ReplyToThread: replyToThread,
ReplyToThread: replyToThread CanReplyToThread: canReplyToThread,
GetMessageLink: getMessageLocation
}, },
actionImpl: { actionImpl: {
ArchiveChannel, ArchiveChannel,
@ -206,6 +206,7 @@ export default async (): Promise<Resources> => ({
ConvertDmToPrivateChannel, ConvertDmToPrivateChannel,
DeleteChatMessage: deleteChatMessage, DeleteChatMessage: deleteChatMessage,
LeaveChannel: leaveChannelAction, LeaveChannel: leaveChannelAction,
RemoveChannel: removeChannelAction RemoveChannel: removeChannelAction,
ReplyToThread: replyToThread
} }
}) })

View File

@ -138,8 +138,6 @@ export default plugin(chunterId, {
ChatMessagePresenter: '' as AnyComponent, ChatMessagePresenter: '' as AnyComponent,
ThreadMessagePresenter: '' as AnyComponent, ThreadMessagePresenter: '' as AnyComponent,
ChatAside: '' as AnyComponent, ChatAside: '' as AnyComponent,
Replies: '' as AnyComponent,
ReplyToThreadAction: '' as AnyComponent,
ChatMessagePreview: '' as AnyComponent, ChatMessagePreview: '' as AnyComponent,
ThreadMessagePreview: '' as AnyComponent ThreadMessagePreview: '' as AnyComponent
}, },

View File

@ -119,6 +119,7 @@
"ToViewCommands": "to view available commands", "ToViewCommands": "to view available commands",
"UnArchive": "Unarchive", "UnArchive": "Unarchive",
"Pin": "Pin", "Pin": "Pin",
"Unpin": "Unpin" "Unpin": "Unpin",
"MoreActions": "More actions"
} }
} }

View File

@ -116,6 +116,7 @@
"ToViewCommands": "чтобы увидеть команды", "ToViewCommands": "чтобы увидеть команды",
"UnArchive": "Разархивировать", "UnArchive": "Разархивировать",
"Pin": "Закрепить", "Pin": "Закрепить",
"Unpin": "Открепить" "Unpin": "Открепить",
"MoreActions": "Больше действий"
} }
} }

View File

@ -194,7 +194,8 @@ const view = plugin(viewId, {
Type: '' as IntlString, Type: '' as IntlString,
UnArchive: '' as IntlString, UnArchive: '' as IntlString,
Save: '' as IntlString, Save: '' as IntlString,
PublicView: '' as IntlString PublicView: '' as IntlString,
MoreActions: '' as IntlString
}, },
icon: { icon: {
Table: '' as Asset, Table: '' as Asset,