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 ActivityMessage,
type ActivityMessageControl,
type ActivityMessageExtension,
type ActivityMessageExtensionKind,
type ActivityMessagePreview,
type ActivityMessagesFilter,
type ActivityReference,
@ -70,7 +68,7 @@ import {
} from '@hcengineering/model'
import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
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 type { Asset, IntlString, Resource } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
@ -210,15 +208,6 @@ export class TDocUpdateMessageViewlet extends TDoc implements DocUpdateMessageVi
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)
export class TActivityExtension extends TDoc implements ActivityExtension {
@Prop(TypeRef(core.class.Class), core.string.Class)
@ -275,7 +264,6 @@ export function createModel (builder: Builder): void {
TTxViewlet,
TActivityDoc,
TActivityMessagesFilter,
TActivityMessageExtension,
TActivityMessage,
TDocUpdateMessage,
TDocUpdateMessageViewlet,
@ -426,6 +414,113 @@ export function createModel (builder: Builder): void {
{ 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

View File

@ -16,7 +16,13 @@ import { activityId, type ActivityMessage, type DocUpdateMessageViewlet } from '
import activity from '@hcengineering/activity-resources/src/plugin'
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
import { type Doc, type Ref } from '@hcengineering/core'
import { type 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'
export default mergeIds(activityId, activity, {
@ -24,7 +30,10 @@ export default mergeIds(activityId, activity, {
Attributes: '' as IntlString,
Pinned: '' as IntlString,
Emoji: '' as IntlString,
Replies: '' as IntlString
Replies: '' as IntlString,
AddReaction: '' as IntlString,
SaveForLater: '' as IntlString,
RemoveFromLater: '' as IntlString
},
filter: {
AttributesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
@ -35,9 +44,28 @@ export default mergeIds(activityId, activity, {
ids: {
ReactionAddedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
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: {
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 }
})
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, {
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, {
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

View File

@ -40,13 +40,15 @@ export default mergeIds(chunterId, chunter, {
ArchiveChannel: '' as Ref<Action>,
UnarchiveChannel: '' as Ref<Action>,
ConvertToPrivate: '' as Ref<Action>,
CopyChatMessageLink: '' as Ref<Action<Doc, any>>
CopyChatMessageLink: '' as Ref<Action<Doc, any>>,
ReplyToThreadAction: '' as Ref<Action>
},
actionImpl: {
ArchiveChannel: '' as ViewAction,
UnarchiveChannel: '' as ViewAction,
ConvertDmToPrivateChannel: '' as ViewAction,
DeleteChatMessage: '' as ViewAction
DeleteChatMessage: '' as ViewAction,
ReplyToThread: '' as ViewAction
},
category: {
Chunter: '' as Ref<ActionCategory>
@ -100,8 +102,9 @@ export default mergeIds(chunterId, chunter, {
CanCopyMessageLink: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
GetChunterSpaceLinkFragment: '' 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: {
ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>

View File

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

View File

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

View File

@ -11,4 +11,7 @@
<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" />
</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>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

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

View File

@ -40,6 +40,9 @@
"Mentions": "Упоминания",
"MentionedYouIn": "Упомянул(а) вас в {title}",
"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, {
Activity: `${icons}#activity`,
Emoji: `${icons}#emoji`,
Bookmark: `${icons}#bookmark`
Bookmark: `${icons}#bookmark`,
BookmarkFilled: `${icons}#bookmark-filled`
})

View File

@ -14,9 +14,10 @@
-->
<script lang="ts">
import { type AnySvelteComponent, ButtonIcon, IconSize } from '@hcengineering/ui'
import { Asset } from '@hcengineering/platform'
import { Asset, IntlString } from '@hcengineering/platform'
import { ComponentType } from 'svelte'
export let label: IntlString
export let icon: Asset | AnySvelteComponent | ComponentType
export let iconProps: any | undefined = undefined
export let size: IconSize = 'small'
@ -30,4 +31,13 @@
}
</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.
-->
<script lang="ts">
import activity, { ActivityMessage, ActivityMessageExtension } from '@hcengineering/activity'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { Action, IconMoreV, showPopup } from '@hcengineering/ui'
import { Menu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { getActions, Menu } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import { getResource } from '@hcengineering/platform'
import view, { Action as ViewAction } from '@hcengineering/view'
import ActivityMessageAction from './ActivityMessageAction.svelte'
import PinMessageAction from './PinMessageAction.svelte'
import SaveMessageAction from './SaveMessageAction.svelte'
import ActivityMessageExtensionComponent from './activity-message/ActivityMessageExtension.svelte'
import AddReactionAction from './reactions/AddReactionAction.svelte'
import { savedMessagesStore } from '../activity'
export let message: ActivityMessage | undefined
export let extensions: ActivityMessageExtension[] = []
export let actions: Action[] = []
export let withActionMenu = true
export let onOpen: () => void
export let onClose: () => void
const dispatch = createEventDispatcher()
const client = getClient()
let inlineActions: ViewAction[] = []
let isActionMenuOpened = false
$: void updateInlineActions(message)
savedMessagesStore.subscribe(() => {
void updateInlineActions(message)
})
function handleActionMenuOpened (): void {
isActionMenuOpened = true
dispatch('open')
onOpen()
}
function handleActionMenuClosed (): void {
isActionMenuOpened = false
dispatch('close')
onClose()
}
function showMenu (ev: MouseEvent): void {
const excludedActions = inlineActions.map(({ _id }) => _id)
showPopup(
Menu,
{
object: message,
actions,
baseMenuClass: activity.class.ActivityMessage
baseMenuClass: activity.class.ActivityMessage,
excludedActions
},
ev.target as HTMLElement,
handleActionMenuClosed
)
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>
{#if message}
<div class="root">
<AddReactionAction object={message} on:open on:close />
<ActivityMessageExtensionComponent kind="action" {extensions} props={{ object: message }} on:close on:open />
<PinMessageAction object={message} />
<SaveMessageAction object={message} />
{#each inlineActions as inline}
{#if inline.icon}
{#await getResource(inline.action) then action}
<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}
<ActivityMessageAction size="small" icon={IconMoreV} opened={isActionMenuOpened} action={showMenu} />
<ActivityMessageAction
size="small"
icon={IconMoreV}
opened={isActionMenuOpened}
action={showMenu}
label={view.string.MoreActions}
/>
{/if}
</div>
{/if}

View File

@ -26,9 +26,10 @@
import core, { Account, Doc, Ref, Timestamp } from '@hcengineering/core'
import { Icon, Label, resizeObserver, TimeSince, tooltip } from '@hcengineering/ui'
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import activity, { ActivityMessagePreviewType } from '@hcengineering/activity'
import { classIcon, DocNavLink } from '@hcengineering/view-resources'
import activity, { ActivityMessage, ActivityMessagePreviewType } from '@hcengineering/activity'
import { classIcon, DocNavLink, showMenu } from '@hcengineering/view-resources'
export let message: ActivityMessage | undefined = undefined
export let text: string | undefined = undefined
export let intlLabel: IntlString | undefined = undefined
export let readonly = false
@ -99,6 +100,12 @@
width = element.clientWidth
}}
on:click
on:contextmenu={(evt) => {
showMenu(evt, { object: message, baseMenuClass: activity.class.ActivityMessage }, () => {
isActionsOpened = false
})
isActionsOpened = true
}}
>
<span class="left overflow-label">
{#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 embedded = false
export let onReply: (() => void) | undefined = undefined
const client = getClient()
const maxDisplayPersons = 5
@ -83,11 +82,6 @@
e.stopPropagation()
e.preventDefault()
if (onReply) {
onReply()
return
}
if (replyProvider) {
const fn = await getResource(replyProvider.function)
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 compact = false
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -70,8 +69,7 @@
videoPreload,
hideLink,
compact,
onClick,
onReply
onClick
}}
/>
{/if}

View File

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

View File

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

View File

@ -43,7 +43,6 @@
export let hideLink = false
export let compact = false
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -112,7 +111,6 @@
{hoverStyles}
showDatePreposition
{onClick}
{onReply}
>
<svelte:fragment slot="header">
<span class="header">

View File

@ -51,7 +51,6 @@
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let hideLink = false
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -172,7 +171,6 @@
{hoverStyles}
showDatePreposition={hideLink}
{onClick}
{onReply}
>
<svelte:fragment slot="header">
<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 { updateReferences } from './references'
import {
addReactionAction,
canPinMessage,
canRemoveFromSaved,
saveForLater,
unpinMessage,
pinMessage,
canSaveForLater,
canUnpinMessage,
removeFromSaved
} from './utils'
export * from './activity'
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 ActivityMessageNotificationLabel } from './components/activity-message/ActivityMessageNotificationLabel.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 ActivityMessagesFilterPopup } from './components/FilterPopup.svelte'
export { default as ActivityReferencePresenter } from './components/activity-reference/ActivityReferencePresenter.svelte'
@ -70,7 +80,20 @@ export default async (): Promise<Resources> => ({
AllFilter: allFilter,
ReferencesFilter: referencesFilter
},
function: {
CanSaveForLater: canSaveForLater,
CanRemoveFromSaved: canRemoveFromSaved,
CanPinMessage: canPinMessage,
CanUnpinMessage: canUnpinMessage
},
backreference: {
Update: updateReferences
},
actionImpl: {
AddReaction: addReactionAction,
SaveForLater: saveForLater,
RemoveFromSaved: removeFromSaved,
PinMessage: pinMessage,
UnpinMessage: unpinMessage
}
})

View File

@ -20,18 +20,23 @@ import core, {
getCurrentAccount
} from '@hcengineering/core'
import { type Asset, type IntlString, getResource, translate } from '@hcengineering/platform'
import { getAttributePresenterClass } from '@hcengineering/presentation'
import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import {
type AnyComponent,
type AnySvelteComponent,
ErrorPresenter,
themeStore,
type Location
type Location,
getEventPositionElement,
closePopup,
showPopup,
EmojiPopup
} from '@hcengineering/ui'
import view, { type AttributeModel, type BuildModelKey, type BuildModelOptions } from '@hcengineering/view'
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'
const valueTypes: ReadonlyArray<Ref<Class<Doc>>> = [
@ -427,3 +432,90 @@ export async function updateDocReactions (
export function getMessageFromLoc (loc: Location): 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> }[]
}
export type ActivityMessageExtensionKind = 'action' | 'footer'
/**
* @public
*/
export interface ActivityMessageExtension extends Doc {
ofMessage: Ref<Class<ActivityMessage>>
components: { kind: ActivityMessageExtensionKind, component: AnyComponent }[]
}
/**
* @public
*/
@ -333,7 +323,6 @@ export default plugin(activityId, {
ActivityInfoMessage: '' as Ref<Class<ActivityInfoMessage>>,
ActivityMessageControl: '' as Ref<Class<ActivityMessageControl>>,
DocUpdateMessageViewlet: '' as Ref<Class<DocUpdateMessageViewlet>>,
ActivityMessageExtension: '' as Ref<Class<ActivityMessageExtension>>,
ActivityMessagesFilter: '' as Ref<Class<ActivityMessagesFilter>>,
ActivityExtension: '' as Ref<Class<ActivityExtension>>,
Reaction: '' as Ref<Class<Reaction>>,
@ -344,7 +333,8 @@ export default plugin(activityId, {
icon: {
Activity: '' as Asset,
Emoji: '' as Asset,
Bookmark: '' as Asset
Bookmark: '' as Asset,
BookmarkFilled: '' as Asset
},
string: {
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 compact = false
export let onClick: (() => void) | undefined = undefined
export let onReply: (() => void) | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -183,7 +182,6 @@
{skipLabel}
showDatePreposition={hideLink}
{onClick}
{onReply}
>
<svelte:fragment slot="header">
<ChatMessageHeader {object} {parentObject} message={value} {viewlet} {person} {skipLabel} {hideLink} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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