mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 11:42:30 +03:00
[Part 2]: Sidebar and chat fixes (#6656)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
25ab61f488
commit
f2c961c5d5
@ -152,6 +152,9 @@ export class TActivityReference extends TActivityMessage implements ActivityRefe
|
||||
@Prop(TypeMarkup(), activity.string.Message)
|
||||
@Index(IndexKind.FullText)
|
||||
message!: string
|
||||
|
||||
@Prop(TypeTimestamp(), activity.string.Edit)
|
||||
editedOn?: Timestamp
|
||||
}
|
||||
|
||||
@Model(activity.class.ActivityInfoMessage, activity.class.ActivityMessage)
|
||||
|
@ -22,7 +22,6 @@ import {
|
||||
TypeMarkup,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
TypeTimestamp,
|
||||
UX
|
||||
} from '@hcengineering/model'
|
||||
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
|
||||
@ -59,6 +58,7 @@ import type { DocNotifyContext } from '@hcengineering/notification'
|
||||
import chunter from './plugin'
|
||||
|
||||
export const DOMAIN_CHUNTER = 'chunter' as Domain
|
||||
|
||||
@Model(chunter.class.ChunterSpace, core.class.Space)
|
||||
export class TChunterSpace extends TSpace implements ChunterSpace {
|
||||
@Prop(PropCollection(activity.class.ActivityMessage), chunter.string.Messages)
|
||||
@ -84,9 +84,6 @@ export class TChatMessage extends TActivityMessage implements ChatMessage {
|
||||
@Index(IndexKind.FullText)
|
||||
message!: string
|
||||
|
||||
@Prop(TypeTimestamp(), chunter.string.Edit)
|
||||
editedOn?: Timestamp
|
||||
|
||||
@Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, {
|
||||
shortLabel: attachment.string.Files
|
||||
})
|
||||
|
@ -321,6 +321,7 @@
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: var(--spacing-6_5);
|
||||
overflow: hidden;
|
||||
|
||||
&.clearPadding > .hulyHeader-row {
|
||||
padding: 0;
|
||||
|
@ -39,6 +39,7 @@
|
||||
export let noPrint: boolean = false
|
||||
export let freezeBefore: boolean = false
|
||||
export let doubleRowWidth = 768
|
||||
export let closeOnEscape: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -61,7 +62,7 @@
|
||||
})
|
||||
|
||||
function _close (ev: KeyboardEvent): void {
|
||||
if (closeButton && ev.key === 'Escape') {
|
||||
if (closeButton && ev.key === 'Escape' && closeOnEscape) {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
|
||||
@ -138,7 +139,9 @@
|
||||
{/if}
|
||||
{#if closeButton}
|
||||
{#if type !== 'type-popup'}<div class="hulyHeader-divider no-print" />{/if}
|
||||
<div class="hulyHotKey-item no-print">Esc</div>
|
||||
{#if closeOnEscape}
|
||||
<div class="hulyHotKey-item no-print">Esc</div>
|
||||
{/if}
|
||||
<ButtonIcon icon={IconClose} kind={'tertiary'} size={'small'} noPrint on:click={() => dispatch('close')} />
|
||||
{/if}
|
||||
</div>
|
||||
@ -233,7 +236,9 @@
|
||||
{/if}
|
||||
{#if closeButton}
|
||||
{#if type !== 'type-popup'}<div class="hulyHeader-divider no-print" />{/if}
|
||||
<div class="hulyHotKey-item no-print">Esc</div>
|
||||
{#if closeOnEscape}
|
||||
<div class="hulyHotKey-item no-print">Esc</div>
|
||||
{/if}
|
||||
<ButtonIcon icon={IconClose} kind={'tertiary'} size={'small'} noPrint on:click={() => dispatch('close')} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -85,6 +85,7 @@
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
color: var(--theme-content-color);
|
||||
|
||||
&.primary {
|
||||
background-color: var(--theme-button-pressed);
|
||||
|
@ -126,6 +126,7 @@ export interface TabItem {
|
||||
}
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
id?: string
|
||||
icon?: Asset | AnySvelteComponent | ComponentType
|
||||
iconProps?: any
|
||||
iconWidth?: string
|
||||
|
@ -26,6 +26,7 @@
|
||||
import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import { Action as ViewAction } from '@hcengineering/view'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
|
||||
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
|
||||
@ -228,6 +229,9 @@
|
||||
<span class="text-sm lower">
|
||||
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
|
||||
</span>
|
||||
{#if message.editedOn}
|
||||
<span class="text-sm lower">(<Label label={notification.string.Edited} />)</span>
|
||||
{/if}
|
||||
|
||||
{#if withActions && inlineActions.length > 0 && !readonly}
|
||||
<div class="flex-presenter flex-gap-2 ml-2">
|
||||
|
@ -46,6 +46,7 @@ export interface ActivityMessage extends AttachedDoc {
|
||||
|
||||
replies?: number
|
||||
reactions?: number
|
||||
editedOn?: Timestamp
|
||||
}
|
||||
|
||||
export type DisplayActivityMessage = DisplayDocUpdateMessage | ActivityMessage
|
||||
@ -81,6 +82,7 @@ export interface ActivityInfoMessage extends ActivityMessage {
|
||||
props?: Record<string, any>
|
||||
icon?: Asset
|
||||
iconProps?: Record<string, any>
|
||||
editedOn?: Timestamp
|
||||
|
||||
// A possible set of links to some platform resources.
|
||||
links?: { _class: Ref<Class<Doc>>, _id: Ref<Doc> }[]
|
||||
|
@ -42,6 +42,7 @@
|
||||
hideExtra={false}
|
||||
adaptive="autoExtra"
|
||||
doubleRowWidth={350}
|
||||
closeOnEscape={false}
|
||||
on:close
|
||||
>
|
||||
<div class="title">
|
||||
|
@ -30,6 +30,7 @@
|
||||
export let filters: Ref<ActivityMessagesFilter>[] = []
|
||||
export let isAsideOpened = false
|
||||
export let syncLocation = true
|
||||
export let freeze = false
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -111,5 +112,6 @@
|
||||
provider={dataProvider}
|
||||
{isAsideOpened}
|
||||
loadMoreAllowed={!isDocChannel}
|
||||
{freeze}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -36,6 +36,7 @@
|
||||
export let isAsideShown: boolean = false
|
||||
export let filters: Ref<ActivityMessagesFilter>[] = []
|
||||
export let canOpenInSidebar: boolean = false
|
||||
export let closeOnEscape: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -80,6 +81,7 @@
|
||||
{isAsideShown}
|
||||
{withSearch}
|
||||
{canOpenInSidebar}
|
||||
{closeOnEscape}
|
||||
on:aside-toggled
|
||||
on:close
|
||||
>
|
||||
|
@ -61,8 +61,9 @@
|
||||
export let skipLabels = false
|
||||
export let loadMoreAllowed = true
|
||||
export let isAsideOpened = false
|
||||
export let initialScrollBottom = true
|
||||
export let fullHeight = true
|
||||
export let fixedInput = true
|
||||
export let freeze = false
|
||||
|
||||
const doc = object
|
||||
|
||||
@ -130,17 +131,21 @@
|
||||
}
|
||||
})
|
||||
|
||||
function isFreeze (): boolean {
|
||||
return freeze
|
||||
}
|
||||
|
||||
$: displayMessages = filterChatMessages(messages, filters, filterResources, doc._class, selectedFilters)
|
||||
|
||||
const unsubscribe = inboxClient.inboxNotificationsByContext.subscribe(() => {
|
||||
if (notifyContext !== undefined) {
|
||||
if (notifyContext !== undefined && !isFreeze()) {
|
||||
recheckNotifications(notifyContext)
|
||||
readViewportMessages()
|
||||
}
|
||||
})
|
||||
|
||||
function scrollToBottom (afterScrollFn?: () => void): void {
|
||||
if (scroller != null && scrollElement != null) {
|
||||
if (scroller != null && scrollElement != null && !isFreeze()) {
|
||||
scroller.scrollBy(scrollElement.scrollHeight)
|
||||
updateSelectedDate()
|
||||
afterScrollFn?.()
|
||||
@ -338,7 +343,7 @@
|
||||
let messagesToReadAccumulatorTimer: any
|
||||
|
||||
function readViewportMessages (): void {
|
||||
if (!scrollElement || !scrollContentBox) {
|
||||
if (!scrollElement || !scrollContentBox || isFreeze()) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -452,22 +457,14 @@
|
||||
isInitialScrolling = false
|
||||
} else if (separatorIndex === -1) {
|
||||
await wait()
|
||||
if (initialScrollBottom) {
|
||||
isScrollInitialized = true
|
||||
shouldWaitAndRead = true
|
||||
autoscroll = true
|
||||
shouldScrollToNew = true
|
||||
isInitialScrolling = false
|
||||
waitLastMessageRenderAndRead(() => {
|
||||
autoscroll = false
|
||||
})
|
||||
} else {
|
||||
isScrollInitialized = true
|
||||
isScrollInitialized = true
|
||||
shouldWaitAndRead = true
|
||||
autoscroll = true
|
||||
shouldScrollToNew = true
|
||||
isInitialScrolling = false
|
||||
waitLastMessageRenderAndRead(() => {
|
||||
autoscroll = false
|
||||
updateShouldScrollToNew()
|
||||
isInitialScrolling = false
|
||||
readViewportMessages()
|
||||
}
|
||||
})
|
||||
} else if (separatorElement) {
|
||||
await wait()
|
||||
scrollToSeparator()
|
||||
@ -519,6 +516,7 @@
|
||||
|
||||
function scrollToNewMessages (): void {
|
||||
if (!scrollElement || !shouldScrollToNew) {
|
||||
readViewportMessages()
|
||||
return
|
||||
}
|
||||
|
||||
@ -557,14 +555,22 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (isFreeze()) {
|
||||
messagesCount = newCount
|
||||
return
|
||||
}
|
||||
|
||||
if (scrollToRestore > 0) {
|
||||
void restoreScroll()
|
||||
} else if (dateToJump !== undefined) {
|
||||
await wait()
|
||||
scrollToDate(dateToJump)
|
||||
} else if (messagesCount > 0 && newCount > messagesCount) {
|
||||
} else if (shouldScrollToNew && messagesCount > 0 && newCount > messagesCount) {
|
||||
await wait()
|
||||
scrollToNewMessages()
|
||||
} else {
|
||||
await wait()
|
||||
readViewportMessages()
|
||||
}
|
||||
|
||||
messagesCount = newCount
|
||||
@ -576,7 +582,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldScrollToNew && initialScrollBottom) {
|
||||
if (shouldScrollToNew) {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
@ -660,7 +666,7 @@
|
||||
} else if (element != null) {
|
||||
const { scrollHeight, scrollTop, offsetHeight } = element
|
||||
|
||||
showScrollDownButton = scrollHeight > offsetHeight + scrollTop + 300
|
||||
showScrollDownButton = scrollHeight > offsetHeight + scrollTop + 50
|
||||
} else {
|
||||
showScrollDownButton = false
|
||||
}
|
||||
@ -691,7 +697,7 @@
|
||||
$: void forceReadContext(isScrollAtBottom, notifyContext)
|
||||
|
||||
async function forceReadContext (isScrollAtBottom: boolean, context?: DocNotifyContext): Promise<void> {
|
||||
if (context === undefined || !isScrollAtBottom || forceRead) return
|
||||
if (context === undefined || !isScrollAtBottom || forceRead || isFreeze()) return
|
||||
const { lastUpdateTimestamp = 0, lastViewedTimestamp = 0 } = context
|
||||
|
||||
if (lastViewedTimestamp >= lastUpdateTimestamp) return
|
||||
@ -708,6 +714,10 @@
|
||||
}
|
||||
|
||||
const canLoadNextForwardStore = provider.canLoadNextForwardStore
|
||||
|
||||
$: if (!freeze) {
|
||||
readViewportMessages()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
@ -777,6 +787,16 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if !fixedInput}
|
||||
<div class="ref-input flex-col">
|
||||
<ActivityExtensionComponent
|
||||
kind="input"
|
||||
{extensions}
|
||||
props={{ object, boundary: scrollElement, collection, autofocus: true, withTypingInfo: true }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loadMoreAllowed && $canLoadNextForwardStore}
|
||||
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
||||
{/if}
|
||||
@ -794,7 +814,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if object}
|
||||
{#if fixedInput && object}
|
||||
<div class="ref-input flex-col">
|
||||
<ActivityExtensionComponent
|
||||
kind="input"
|
||||
@ -841,7 +861,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
bottom: -0.75rem;
|
||||
animation: 1s fadeIn;
|
||||
animation: 0.5s fadeIn;
|
||||
animation-fill-mode: forwards;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
@ -58,9 +58,14 @@
|
||||
{ limit: 1 }
|
||||
)
|
||||
}
|
||||
let renderChannel = tab.data.thread === undefined
|
||||
|
||||
$: if (tab.data.thread === undefined) {
|
||||
renderChannel = true
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
{#if object && renderChannel}
|
||||
<div class="channel" class:invisible={threadId !== undefined} style:height style:width>
|
||||
<ChannelHeader
|
||||
_id={object._id}
|
||||
@ -71,16 +76,17 @@
|
||||
canOpen={true}
|
||||
allowClose={true}
|
||||
canOpenInSidebar={false}
|
||||
closeOnEscape={false}
|
||||
on:close
|
||||
/>
|
||||
{#key object._id}
|
||||
<Channel {object} {context} syncLocation={false} />
|
||||
<Channel {object} {context} syncLocation={false} freeze={threadId !== undefined} />
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{#if threadId}
|
||||
<div class="thread" style:height style:width>
|
||||
<ThreadView _id={threadId} on:close={() => closeThreadInSidebarChannel(widget, tab)} />
|
||||
<ThreadView _id={threadId} on:channel={() => closeThreadInSidebarChannel(widget, tab)} on:close />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -93,7 +93,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="popupPanel panel">
|
||||
<div class="popupPanel">
|
||||
<ChannelHeader
|
||||
_id={object._id}
|
||||
_class={object._class}
|
||||
|
@ -36,7 +36,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if widget && tab && tab.type === 'channel'}
|
||||
{#if widget && tab}
|
||||
<ChannelSidebarView
|
||||
{widget}
|
||||
{tab}
|
||||
@ -46,13 +46,4 @@
|
||||
handleClose(tab?.id)
|
||||
}}
|
||||
/>
|
||||
{:else if widget && tab && tab.type === 'thread'}
|
||||
<ThreadSidebarView
|
||||
{tab}
|
||||
{height}
|
||||
{width}
|
||||
on:close={() => {
|
||||
handleClose(tab?.id)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -50,7 +50,7 @@
|
||||
|
||||
let count: number = 0
|
||||
|
||||
$: objectId = tab.type === 'thread' ? tab.data.thread : tab.data._id
|
||||
$: objectId = tab.data.thread ?? tab.data._id
|
||||
$: context = objectId ? $contextByDocStore.get(objectId) : undefined
|
||||
|
||||
const unsubscribe = notificationClient.inboxNotificationsByContext.subscribe((res) => {
|
||||
|
@ -57,6 +57,7 @@
|
||||
export let adaptive: HeaderAdaptive = 'default'
|
||||
export let hideActions: boolean = false
|
||||
export let canOpenInSidebar: boolean = false
|
||||
export let closeOnEscape: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -72,6 +73,7 @@
|
||||
hideActions={!((canOpen && object) || withAside || $$slots.actions) || hideActions}
|
||||
hideDescription={!description}
|
||||
adaptive={adaptive !== 'default' ? adaptive : withFilters ? 'freezeActions' : 'disabled'}
|
||||
{closeOnEscape}
|
||||
on:click
|
||||
on:close
|
||||
>
|
||||
|
@ -33,7 +33,7 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={div}
|
||||
class="border-radius-4 over-underline dateSelectorButton clear-mins"
|
||||
class="border-radius-4 dateSelectorButton clear-mins"
|
||||
on:click={() => {
|
||||
showPopup(DateRangePopup, {}, div, (v) => {
|
||||
if (v) {
|
||||
@ -68,13 +68,17 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--theme-divider-color);
|
||||
background-color: var(--highlight-select-border);
|
||||
}
|
||||
|
||||
.dateSelectorButton {
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
height: max-content;
|
||||
background-color: var(--theme-list-row-color);
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
color: var(--theme-content-color);
|
||||
background-color: var(--highlight-select);
|
||||
border: 1px solid var(--highlight-select-border);
|
||||
font-weight: 500;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
@ -14,20 +14,15 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Timestamp } from '@hcengineering/core'
|
||||
|
||||
export let editedOn: Timestamp | undefined
|
||||
export let label: IntlString | undefined
|
||||
</script>
|
||||
|
||||
{#if label}
|
||||
<span class="text-sm lower"> <Label {label} /></span>
|
||||
{/if}
|
||||
{#if editedOn}
|
||||
<span class="text-sm lower"><Label label={notification.string.Edited} /></span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
|
@ -255,7 +255,7 @@
|
||||
{onClick}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<ChatMessageHeader editedOn={value.editedOn} label={skipLabel ? undefined : viewlet?.label} />
|
||||
<ChatMessageHeader label={viewlet?.label} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if !isEditing}
|
||||
|
@ -76,7 +76,7 @@
|
||||
|
||||
if (direct !== undefined && missingAccounts.length > 0) {
|
||||
await client.updateDoc(chunter.class.DirectMessage, direct.space, direct._id, {
|
||||
members: [...direct.members, ...missingAccounts]
|
||||
$push: { members: { $each: missingAccounts, $position: 0 } }
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
||||
|
||||
{#if tab.data.thread}
|
||||
<div class="root" style:height style:width>
|
||||
<ThreadView _id={tab.data.thread} on:close />
|
||||
<ThreadView _id={tab.data.thread} on:close on:channel />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Breadcrumbs, Label, location as locationStore, Header } from '@hcengineering/ui'
|
||||
import { Breadcrumbs, Label, location as locationStore, Header, BreadcrumbItem } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import { getMessageFromLoc, messageInFocus } from '@hcengineering/activity-resources'
|
||||
@ -104,7 +104,14 @@
|
||||
channelName = res
|
||||
})
|
||||
|
||||
function getBreadcrumbsItems (channel?: Doc, message?: DisplayActivityMessage, channelName?: string) {
|
||||
let breadcrumbs: BreadcrumbItem[] = []
|
||||
$: breadcrumbs = showHeader ? getBreadcrumbsItems(channel, message, channelName) : []
|
||||
|
||||
function getBreadcrumbsItems (
|
||||
channel?: Doc,
|
||||
message?: DisplayActivityMessage,
|
||||
channelName?: string
|
||||
): BreadcrumbItem[] {
|
||||
if (message === undefined) {
|
||||
return []
|
||||
}
|
||||
@ -115,6 +122,7 @@
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'channel',
|
||||
icon: getObjectIcon(message.attachedToClass),
|
||||
iconProps: { value: channel },
|
||||
iconWidth: isPersonAvatar ? 'auto' : undefined,
|
||||
@ -122,14 +130,24 @@
|
||||
title: channelName,
|
||||
label: channelName ? undefined : chunter.string.Channel
|
||||
},
|
||||
{ label: chunter.string.Thread }
|
||||
{ id: 'thread', label: chunter.string.Thread }
|
||||
]
|
||||
}
|
||||
|
||||
function handleBreadcrumbSelect (event: CustomEvent<number>): void {
|
||||
const index = event.detail
|
||||
const breadcrumb = breadcrumbs[index]
|
||||
|
||||
if (breadcrumb === undefined) return
|
||||
if (breadcrumb.id !== 'channel') return
|
||||
|
||||
dispatch('channel')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showHeader}
|
||||
<Header type={'type-aside'} adaptive={'disabled'} on:close>
|
||||
<Breadcrumbs items={getBreadcrumbsItems(channel, message, channelName)} currentOnly />
|
||||
<Header type={'type-aside'} adaptive={'disabled'} closeOnEscape={false} on:close>
|
||||
<Breadcrumbs items={breadcrumbs} on:select={handleBreadcrumbSelect} selected={1} />
|
||||
</Header>
|
||||
{/if}
|
||||
|
||||
@ -141,8 +159,8 @@
|
||||
skipLabels
|
||||
object={message}
|
||||
provider={dataProvider}
|
||||
initialScrollBottom={false}
|
||||
fullHeight={false}
|
||||
fixedInput={false}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="mt-3">
|
||||
|
@ -61,6 +61,7 @@ import {
|
||||
getThreadLink,
|
||||
openChannelInSidebar,
|
||||
openChannelInSidebarAction,
|
||||
openThreadInSidebar,
|
||||
replyToThread
|
||||
} from './navigation'
|
||||
import {
|
||||
@ -199,7 +200,8 @@ export default async (): Promise<Resources> => ({
|
||||
GetMessageLink: getMessageLocation,
|
||||
CloseChatWidgetTab: closeChatWidgetTab,
|
||||
OpenChannelInSidebar: openChannelInSidebar,
|
||||
CanTranslateMessage: canTranslateMessage
|
||||
CanTranslateMessage: canTranslateMessage,
|
||||
OpenThreadInSidebar: openThreadInSidebar
|
||||
},
|
||||
actionImpl: {
|
||||
ArchiveChannel,
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
type Location,
|
||||
navigate
|
||||
} from '@hcengineering/ui'
|
||||
import { type Ref, type Doc, type Class } from '@hcengineering/core'
|
||||
import { type Ref, type Doc, type Class, generateId } from '@hcengineering/core'
|
||||
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
||||
import {
|
||||
type Channel,
|
||||
@ -262,11 +262,11 @@ export async function openChannelInSidebar (
|
||||
size: isDirect || isPerson ? 'tiny' : 'x-small',
|
||||
compact: true
|
||||
},
|
||||
type: 'channel',
|
||||
data: {
|
||||
_id,
|
||||
_class,
|
||||
thread
|
||||
thread,
|
||||
channelName: name
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,18 +288,26 @@ export async function openThreadInSidebarChannel (
|
||||
): Promise<void> {
|
||||
const newTab: ChatWidgetTab = {
|
||||
...tab,
|
||||
name: await translate(chunter.string.ThreadIn, { name: tab.data.channelName }),
|
||||
data: { ...tab.data, thread: message._id }
|
||||
}
|
||||
createWidgetTab(widget, newTab)
|
||||
}
|
||||
|
||||
export async function closeThreadInSidebarChannel (widget: Widget, tab: ChatWidgetTab): Promise<void> {
|
||||
const thread = tab.allowedPath !== undefined ? tab.data.thread : undefined
|
||||
const newTab: ChatWidgetTab = {
|
||||
...tab,
|
||||
id: tab.id.startsWith('thread_') ? generateId() : tab.id,
|
||||
name: tab.data.channelName,
|
||||
allowedPath: undefined,
|
||||
data: { ...tab.data, thread: undefined }
|
||||
}
|
||||
|
||||
createWidgetTab(widget, newTab)
|
||||
setTimeout(() => {
|
||||
removeThreadFromLoc(thread)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc): Promise<void> {
|
||||
@ -321,9 +329,7 @@ export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: Acti
|
||||
const allowedPath = loc.path.join('/')
|
||||
|
||||
const currentTAbs = get(sidebarStore).widgetsState.get(widget._id)?.tabs ?? []
|
||||
const tabsToClose = currentTAbs
|
||||
.filter((t) => t.isPinned !== true && t.allowedPath === allowedPath && (t as ChatWidgetTab).type === 'thread')
|
||||
.map((t) => t.id)
|
||||
const tabsToClose = currentTAbs.filter((t) => t.isPinned !== true && t.allowedPath === allowedPath).map((t) => t.id)
|
||||
|
||||
if (tabsToClose.length > 0) {
|
||||
sidebarStore.update((s) => {
|
||||
@ -341,26 +347,31 @@ export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: Acti
|
||||
name: tabName,
|
||||
icon: chunter.icon.Thread,
|
||||
allowedPath,
|
||||
type: 'thread',
|
||||
data: {
|
||||
_id: object?._id,
|
||||
_class: object?._class,
|
||||
thread: message._id
|
||||
thread: message._id,
|
||||
channelName: name
|
||||
}
|
||||
}
|
||||
createWidgetTab(widget, tab, true)
|
||||
}
|
||||
|
||||
export function closeChatWidgetTab (tab?: ChatWidgetTab): void {
|
||||
if (tab?.type === 'thread') {
|
||||
const loc = getCurrentLocation()
|
||||
export function removeThreadFromLoc (thread?: Ref<ActivityMessage>): void {
|
||||
if (thread === undefined) return
|
||||
const loc = getCurrentLocation()
|
||||
|
||||
if (loc.path[2] === chunterId || loc.path[2] === notificationId) {
|
||||
if (loc.path[4] === tab.data.thread) {
|
||||
loc.path[4] = ''
|
||||
loc.path.length = 4
|
||||
navigate(loc)
|
||||
}
|
||||
if (loc.path[2] === chunterId || loc.path[2] === notificationId) {
|
||||
if (loc.path[4] === thread) {
|
||||
loc.path[4] = ''
|
||||
loc.path.length = 4
|
||||
navigate(loc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function closeChatWidgetTab (tab?: ChatWidgetTab): void {
|
||||
if (tab?.allowedPath !== undefined) {
|
||||
removeThreadFromLoc(tab.data.thread)
|
||||
}
|
||||
}
|
||||
|
@ -434,9 +434,13 @@ export async function readChannelMessages (
|
||||
return
|
||||
}
|
||||
|
||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||
if (context === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||
const client = getClient().apply(undefined, 'readViewportMessages')
|
||||
|
||||
try {
|
||||
const readMessages = get(chatReadMessagesStore)
|
||||
const allIds = getAllIds(messages).filter((id) => !readMessages.has(id))
|
||||
@ -460,12 +464,6 @@ export async function readChannelMessages (
|
||||
|
||||
chatReadMessagesStore.update((store) => new Set([...store, ...allIds]))
|
||||
|
||||
await inboxClient.readNotifications(client, [...notifications, ...relatedMentions])
|
||||
|
||||
if (context === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const storedTimestampUpdates = get(contextsTimestampStore).get(context._id)
|
||||
const newTimestamp = messages[messages.length - 1].createdOn ?? 0
|
||||
const prevTimestamp = Math.max(storedTimestampUpdates ?? 0, context.lastViewedTimestamp ?? 0)
|
||||
@ -478,6 +476,7 @@ export async function readChannelMessages (
|
||||
})
|
||||
await client.update(context, { lastViewedTimestamp: newTimestamp })
|
||||
}
|
||||
await inboxClient.readNotifications(client, [...notifications, ...relatedMentions])
|
||||
} finally {
|
||||
await client.commit()
|
||||
}
|
||||
|
@ -100,8 +100,7 @@ export interface InlineButton extends AttachedDoc {
|
||||
}
|
||||
|
||||
export interface ChatWidgetTab extends WidgetTab {
|
||||
type: 'channel' | 'thread'
|
||||
data: { _id?: Ref<Doc>, _class?: Ref<Class<Doc>>, thread?: Ref<ActivityMessage> }
|
||||
data: { _id?: Ref<Doc>, _class?: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>, channelName: string }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -232,6 +231,7 @@ export default plugin(chunterId, {
|
||||
},
|
||||
function: {
|
||||
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
OpenThreadInSidebar: '' as Resource<(_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc) => Promise<void>>,
|
||||
OpenChannelInSidebar: '' as Resource<
|
||||
(_id: Ref<Doc>, _class: Ref<Doc>, doc?: Doc, thread?: Ref<ActivityMessage>) => Promise<void>
|
||||
>
|
||||
|
@ -37,6 +37,7 @@
|
||||
import view, { decodeObjectURI } from '@hcengineering/view'
|
||||
import { parseLinkId } from '@hcengineering/view-resources'
|
||||
import { get } from 'svelte/store'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
||||
import notification from '../../plugin'
|
||||
@ -179,6 +180,12 @@
|
||||
}
|
||||
|
||||
const selectedMessageId = loc?.loc.query?.message as Ref<ActivityMessage> | undefined
|
||||
const thread = loc?.loc.path[4] as Ref<ActivityMessage> | undefined
|
||||
|
||||
if (thread !== undefined) {
|
||||
const fn = await getResource(chunter.function.OpenThreadInSidebar)
|
||||
void fn(thread)
|
||||
}
|
||||
|
||||
if (selectedMessageId !== undefined) {
|
||||
selectedMessage = get(inboxClient.activityInboxNotifications).find(
|
||||
|
@ -588,6 +588,7 @@ export async function selectInboxContext (
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const { objectId, objectClass } = context
|
||||
const loc = getCurrentLocation()
|
||||
|
||||
if (isMentionNotification(notification) && isActivityMessageClass(notification.mentionedInClass)) {
|
||||
const selectedMsg = notification.mentionedIn as Ref<ActivityMessage>
|
||||
@ -619,17 +620,25 @@ export async function selectInboxContext (
|
||||
}
|
||||
|
||||
if (isReactionMessage(message)) {
|
||||
void navigateToInboxDoc(linkProviders, objectId, objectClass, undefined, objectId as Ref<ActivityMessage>)
|
||||
const thread = loc.path[4] === objectId ? objectId : undefined
|
||||
void navigateToInboxDoc(
|
||||
linkProviders,
|
||||
objectId,
|
||||
objectClass,
|
||||
thread as Ref<ActivityMessage>,
|
||||
objectId as Ref<ActivityMessage>
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const selectedMsg = (notification as ActivityInboxNotification)?.attachedTo
|
||||
const thread = selectedMsg !== objectId ? objectId : loc.path[4] === objectId ? objectId : undefined
|
||||
|
||||
void navigateToInboxDoc(
|
||||
linkProviders,
|
||||
objectId,
|
||||
objectClass,
|
||||
selectedMsg !== undefined ? (objectId as Ref<ActivityMessage>) : undefined,
|
||||
thread as Ref<ActivityMessage>,
|
||||
selectedMsg ?? (objectId as Ref<ActivityMessage>)
|
||||
)
|
||||
return
|
||||
|
@ -100,6 +100,7 @@
|
||||
hideActions={false}
|
||||
hideDescription={true}
|
||||
adaptive="disabled"
|
||||
closeOnEscape={false}
|
||||
on:close={() => {
|
||||
if (widget !== undefined) {
|
||||
closeWidget(widget._id)
|
||||
@ -168,5 +169,6 @@
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
</style>
|
||||
|
@ -101,7 +101,12 @@ export function openWidget (widget: Widget, data?: Record<string, any>, active =
|
||||
const { widgetsState } = state
|
||||
const widgetState = widgetsState.get(widget._id)
|
||||
|
||||
widgetsState.set(widget._id, { _id: widget._id, data, tab: widgetState?.tab, tabs: widgetState?.tabs ?? [] })
|
||||
widgetsState.set(widget._id, {
|
||||
_id: widget._id,
|
||||
data: data ?? widgetState?.data,
|
||||
tab: widgetState?.tab,
|
||||
tabs: widgetState?.tabs ?? []
|
||||
})
|
||||
|
||||
sidebarStore.set({
|
||||
...state,
|
||||
|
Loading…
Reference in New Issue
Block a user