UBERF-5024: add reactions control to inbox (#4414)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-01-23 19:22:56 +04:00 committed by GitHub
parent 04b3103372
commit 57409feb99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 128 additions and 75 deletions

View File

@ -244,7 +244,7 @@ function groupByTime<T extends ActivityMessage> (messages: T[]): T[][] {
function getDocUpdateMessageKey (message: DocUpdateMessage): string { function getDocUpdateMessageKey (message: DocUpdateMessage): string {
const personAccountById = get(personAccountByIdStore) const personAccountById = get(personAccountByIdStore)
const person = personAccountById.get(message.modifiedBy as any)?.person ?? message.modifiedBy const person = personAccountById.get(message.createdBy as any)?.person ?? message.createdBy
if (message.action === 'update') { if (message.action === 'update') {
return [message._class, message.attachedTo, message.action, person, getAttributeUpdatesKey(message)].join('_') return [message._class, message.attachedTo, message.action, person, getAttributeUpdatesKey(message)].join('_')

View File

@ -86,6 +86,7 @@
} }
const loc = getLocation() const loc = getLocation()
loc.path[4] = message._id loc.path[4] = message._id
loc.query = { ...loc.query, thread: message._id }
navigate(loc) navigate(loc)
} }
</script> </script>

View File

@ -178,8 +178,9 @@
class:opened={isActionMenuOpened || message.isPinned} class:opened={isActionMenuOpened || message.isPinned}
> >
{#if withActions} {#if withActions}
<AddReactionAction object={message} on:open={handleActionMenuOpened} on:close={handleActionMenuClosed} />
{#if withFlatActions} {#if withFlatActions}
<AddReactionAction object={message} />
<PinMessageAction object={message} /> <PinMessageAction object={message} />
<SaveMessageAction object={message} /> <SaveMessageAction object={message} />
@ -231,11 +232,9 @@
} }
&.embedded { &.embedded {
padding: 0; padding: 0.75rem 0 0 0;
gap: 0.25rem;
.content { border-radius: 0;
padding: 0.75rem 0.75rem 0.75rem 0;
}
} }
.actions { .actions {
@ -292,7 +291,7 @@
} }
.embeddedMarker { .embeddedMarker {
width: 6px; width: 0.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
background: var(--secondary-button-default); background: var(--secondary-button-default);
} }

View File

@ -41,8 +41,10 @@
function handleReply (): void { function handleReply (): void {
const loc = getLocation() const loc = getLocation()
loc.fragment = notification.docNotifyContext loc.fragment = notification.docNotifyContext
loc.query = { message: notification.attachedTo } loc.query = { thread: parentMessage?._id ?? message._id }
navigate(loc) navigate(loc)
} }
</script> </script>

View File

@ -38,6 +38,7 @@ 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 async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
component: { component: {

View File

@ -48,7 +48,7 @@
onDestroy(unsubscribe) onDestroy(unsubscribe)
$: isDocChannel = !hierarchy.isDerived(context.attachedToClass, chunter.class.ChunterSpace) $: isDocChannel = !hierarchy.isDerived(object._class, chunter.class.ChunterSpace)
$: messagesClass = isDocChannel ? activity.class.ActivityMessage : chunter.class.ChatMessage $: messagesClass = isDocChannel ? activity.class.ActivityMessage : chunter.class.ChatMessage
$: collection = isDocChannel ? 'comments' : 'messages' $: collection = isDocChannel ? 'comments' : 'messages'
</script> </script>

View File

@ -13,13 +13,13 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Class, Ref } from '@hcengineering/core' import { Class, Ref, WithLookup } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification' import { ActivityInboxNotification, DocNotifyContext } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import activity, { ActivityMessage } from '@hcengineering/activity' import activity, { ActivityMessage } from '@hcengineering/activity'
import { ChunterSpace } from '@hcengineering/chunter' import { ChunterSpace } from '@hcengineering/chunter'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources' import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { location as locationStore } from '@hcengineering/ui' import { location as locationStore, Location } from '@hcengineering/ui'
import { isReactionMessage } from '@hcengineering/activity-resources' import { isReactionMessage } from '@hcengineering/activity-resources'
import ChannelPresenter from './ChannelView.svelte' import ChannelPresenter from './ChannelView.svelte'
@ -37,10 +37,13 @@
let object: ChunterSpace | undefined = undefined let object: ChunterSpace | undefined = undefined
let threadId: Ref<ActivityMessage> | undefined = undefined let threadId: Ref<ActivityMessage> | undefined = undefined
let notification: WithLookup<ActivityInboxNotification> | undefined = undefined
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
let loc: Location | undefined = undefined
locationStore.subscribe((newLocation) => { locationStore.subscribe((newLocation) => {
loc = newLocation
selectedMessageId = newLocation.query?.message as Ref<ActivityMessage> | undefined selectedMessageId = newLocation.query?.message as Ref<ActivityMessage> | undefined
}) })
@ -48,11 +51,25 @@
? $activityInboxNotificationsStore.find(({ attachedTo }) => attachedTo === selectedMessageId) ? $activityInboxNotificationsStore.find(({ attachedTo }) => attachedTo === selectedMessageId)
: undefined : undefined
$: threadId = $: updateThreadId(context, notification, loc)
hierarchy.isDerived(context.attachedToClass, activity.class.ActivityMessage) &&
!isReactionMessage(notification?.$lookup?.attachedTo) function updateThreadId (
? (context.attachedTo as Ref<ActivityMessage>) context: DocNotifyContext,
: undefined notification?: WithLookup<ActivityInboxNotification>,
loc?: Location
) {
threadId = loc?.query?.thread as Ref<ActivityMessage> | undefined
if (threadId !== undefined) {
return
}
threadId =
hierarchy.isDerived(context.attachedToClass, activity.class.ActivityMessage) &&
!isReactionMessage(notification?.$lookup?.attachedTo)
? (context.attachedTo as Ref<ActivityMessage>)
: undefined
}
$: objectQuery.query(_class, { _id }, (res) => { $: objectQuery.query(_class, { _id }, (res) => {
object = res[0] object = res[0]

View File

@ -189,6 +189,7 @@ export async function deleteChatMessage (message: ChatMessage): Promise<void> {
export async function replyToThread (message: ActivityMessage): Promise<void> { export async function replyToThread (message: ActivityMessage): Promise<void> {
const loc = getLocation() const loc = getLocation()
loc.path[4] = message._id loc.path[4] = message._id
loc.query = { ...loc.query, thread: message._id }
navigate(loc) navigate(loc)
} }

View File

@ -15,6 +15,7 @@
<script lang="ts"> <script lang="ts">
import { ActionIcon, Component, IconMoreH, Label, showPopup } from '@hcengineering/ui' import { ActionIcon, Component, IconMoreH, Label, showPopup } from '@hcengineering/ui'
import notification, { import notification, {
ActivityInboxNotification,
ActivityNotificationViewlet, ActivityNotificationViewlet,
DisplayInboxNotification, DisplayInboxNotification,
DocNotifyContext DocNotifyContext
@ -23,19 +24,24 @@
import { getDocTitle, getDocIdentifier, Menu } from '@hcengineering/view-resources' import { getDocTitle, getDocIdentifier, Menu } from '@hcengineering/view-resources'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { WithLookup } from '@hcengineering/core'
import { AddReactionAction } from '@hcengineering/activity-resources'
import InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte' import InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte'
import NotifyContextIcon from './NotifyContextIcon.svelte' import NotifyContextIcon from './NotifyContextIcon.svelte'
import NotifyMarker from './NotifyMarker.svelte' import NotifyMarker from './NotifyMarker.svelte'
export let value: DocNotifyContext export let value: DocNotifyContext
export let notifications: DisplayInboxNotification[] = [] export let notifications: WithLookup<DisplayInboxNotification>[] = []
export let viewlets: ActivityNotificationViewlet[] = [] export let viewlets: ActivityNotificationViewlet[] = []
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let idTitle: string | undefined
let title: string | undefined
$: visibleNotification = notifications[0] $: visibleNotification = notifications[0]
function showMenu (ev: MouseEvent): void { function showMenu (ev: MouseEvent): void {
@ -50,15 +56,40 @@
chunter.action.OpenChannel chunter.action.OpenChannel
] ]
}, },
ev.target as HTMLElement ev.target as HTMLElement,
handleActionMenuClosed
) )
handleActionMenuOpened()
} }
const presenterMixin = hierarchy.classHierarchyMixin( $: presenterMixin = hierarchy.classHierarchyMixin(
value.attachedToClass, value.attachedToClass,
notification.mixin.NotificationContextPresenter notification.mixin.NotificationContextPresenter
) )
$: isCompact = notifications.length === 1 $: isCompact = notifications.length === 1
$: message =
visibleNotification._class === notification.class.ActivityInboxNotification
? (visibleNotification as WithLookup<ActivityInboxNotification>).$lookup?.attachedTo
: undefined
let isActionMenuOpened = false
function handleActionMenuOpened (): void {
isActionMenuOpened = true
}
function handleActionMenuClosed (): void {
isActionMenuOpened = false
}
$: getDocIdentifier(client, value.attachedTo, value.attachedToClass).then((res) => {
idTitle = res
})
$: getDocTitle(client, value.attachedTo, value.attachedToClass).then((res) => {
title = res
})
</script> </script>
{#if visibleNotification} {#if visibleNotification}
@ -74,13 +105,15 @@
> >
{#if isCompact} {#if isCompact}
<InboxNotificationPresenter value={visibleNotification} {viewlets} showNotify={false} withActions={false} /> <InboxNotificationPresenter value={visibleNotification} {viewlets} showNotify={false} withActions={false} />
<div class="actions compact">
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
</div>
<div class="notifyMarker compact"> <div class="notifyMarker compact">
<NotifyMarker count={unreadCount} /> <NotifyMarker count={unreadCount} />
</div> </div>
<div class="actions clear-mins flex flex-gap-2 items-center" class:opened={isActionMenuOpened}>
{#if message}
<AddReactionAction object={message} on:open={handleActionMenuOpened} on:close={handleActionMenuClosed} />
{/if}
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
</div>
{:else} {:else}
<div class="header"> <div class="header">
<!-- <CheckBox--> <!-- <CheckBox-->
@ -96,30 +129,23 @@
<Component is={presenterMixin.labelPresenter} props={{ notification: visibleNotification, context: value }} /> <Component is={presenterMixin.labelPresenter} props={{ notification: visibleNotification, context: value }} />
{:else} {:else}
<div class="labels"> <div class="labels">
{#await getDocIdentifier(client, value.attachedTo, value.attachedToClass) then title} {#if idTitle}
{#if title} {idTitle}
{title} {:else}
{:else} <Label label={hierarchy.getClass(value.attachedToClass).label} />
<Label label={hierarchy.getClass(value.attachedToClass).label} /> {/if}
{/if} <div class="title overflow-label" {title}>
{/await} {title ?? hierarchy.getClass(value.attachedToClass).label}
{#await getDocTitle(client, value.attachedTo, value.attachedToClass) then title} </div>
<div class="title overflow-label" {title}>
{title ?? hierarchy.getClass(value.attachedToClass).label}
</div>
{/await}
</div> </div>
{/if} {/if}
<div class="actions">
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
</div>
<div class="notifyMarker">
<NotifyMarker count={unreadCount} />
</div>
</div> </div>
<div class="actions clear-mins flex flex-gap-2 items-center" class:opened={isActionMenuOpened}>
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
</div>
<div class="notifyMarker">
<NotifyMarker count={unreadCount} />
</div>
<div class="notification"> <div class="notification">
<InboxNotificationPresenter value={visibleNotification} {viewlets} embedded skipLabel /> <InboxNotificationPresenter value={visibleNotification} {viewlets} embedded skipLabel />
</div> </div>
@ -135,7 +161,8 @@
cursor: pointer; cursor: pointer;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 0 1rem; padding: 0.5rem 1rem;
padding-right: 0;
margin: 0.5rem 0; margin: 0.5rem 0;
&.compact { &.compact {
@ -155,6 +182,22 @@
font-weight: 500; font-weight: 500;
max-width: 20.5rem; max-width: 20.5rem;
} }
.actions {
position: absolute;
visibility: hidden;
top: 0.75rem;
right: 0.75rem;
color: var(--theme-halfcontent-color);
&.opened {
visibility: visible;
}
}
&:hover > .actions {
visibility: visible;
}
} }
.labels { .labels {
@ -163,28 +206,17 @@
} }
.notification { .notification {
margin-top: 1rem; margin-top: 0.25rem;
margin-left: 4rem; margin-left: 4rem;
} }
.notifyMarker { .notifyMarker {
position: absolute; position: absolute;
right: 1.875rem; left: 0.25rem;
top: 0; top: 0;
&.compact { &.compact {
right: 2.875rem; left: 0.25rem;
top: 0.5rem;
}
}
.actions {
position: absolute;
right: 0;
top: 0;
&.compact {
right: 1rem;
top: 0.5rem; top: 0.5rem;
} }
} }

View File

@ -40,8 +40,8 @@
const client = getClient() const client = getClient()
const messagesQuery = createQuery() const messagesQuery = createQuery()
const notificationsClient = InboxNotificationsClientImpl.getClient() const inboxClient = InboxNotificationsClientImpl.getClient()
const notificationsStore = notificationsClient.inboxNotifications const notificationsStore = inboxClient.inboxNotifications
let messages: ActivityMessage[] = [] let messages: ActivityMessage[] = []
let viewlet: ActivityNotificationViewlet | undefined = undefined let viewlet: ActivityNotificationViewlet | undefined = undefined
@ -81,6 +81,7 @@
function updateViewlet (viewlets: ActivityNotificationViewlet[], message?: DisplayActivityMessage) { function updateViewlet (viewlets: ActivityNotificationViewlet[], message?: DisplayActivityMessage) {
if (viewlets.length === 0 || message === undefined) { if (viewlets.length === 0 || message === undefined) {
viewlet = undefined
return return
} }
@ -91,6 +92,8 @@
return return
} }
} }
viewlet = undefined
} }
function handleReply (message?: DisplayActivityMessage): void { function handleReply (message?: DisplayActivityMessage): void {
@ -99,7 +102,7 @@
} }
const loc = getLocation() const loc = getLocation()
loc.fragment = value.docNotifyContext loc.fragment = value.docNotifyContext
loc.query = { message: message._id } loc.query = { thread: message._id }
navigate(loc) navigate(loc)
} }

View File

@ -96,6 +96,10 @@
$: filteredNotifications = filterNotifications(selectedTabId, displayNotifications, $notifyContextsStore) $: filteredNotifications = filterNotifications(selectedTabId, displayNotifications, $notifyContextsStore)
locationStore.subscribe((newLocation) => {
syncLocation(newLocation)
})
async function syncLocation (newLocation: Location) { async function syncLocation (newLocation: Location) {
const loc = await resolveLocation(newLocation) const loc = await resolveLocation(newLocation)

View File

@ -49,16 +49,9 @@ import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
* @public * @public
*/ */
export async function hasMarkAsUnreadAction (doc: DisplayInboxNotification): Promise<boolean> { export async function hasMarkAsUnreadAction (doc: DisplayInboxNotification): Promise<boolean> {
const inboxNotificationsClient = InboxNotificationsClientImpl.getClient() const canRead = await hasMarkAsReadAction(doc)
const combinedIds = return !canRead
doc._class === notification.class.ActivityInboxNotification
? (doc as DisplayActivityInboxNotification).combinedIds
: [doc._id]
return get(inboxNotificationsClient.inboxNotifications).some(
({ _id, isViewed }) => combinedIds.includes(_id) && isViewed
)
} }
export async function hasMarkAsReadAction (doc: DisplayInboxNotification): Promise<boolean> { export async function hasMarkAsReadAction (doc: DisplayInboxNotification): Promise<boolean> {
@ -353,11 +346,11 @@ async function generateLocation (
loc: { loc: {
path: [appComponent, workspace, inboxId], path: [appComponent, workspace, inboxId],
fragment: contextId, fragment: contextId,
query: { message: message !== undefined ? (messageId as string) : null } query: { ...loc.query, message: message !== undefined ? (messageId as string) : null }
}, },
defaultLocation: { defaultLocation: {
path: [appComponent, workspace, inboxId], path: [appComponent, workspace, inboxId],
query: { message: message !== undefined ? (messageId as string) : null } query: { ...loc.query, message: message !== undefined ? (messageId as string) : null }
} }
} }
} }