Qfix sidebar (#6708)

This commit is contained in:
Kristina 2024-09-24 16:31:34 +04:00 committed by GitHub
parent 3662f1f8ee
commit 822731c23c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 238 additions and 113 deletions

View File

@ -105,6 +105,10 @@ export class TBrowserNotification extends TDoc implements BrowserNotification {
onClickLocation?: Location | undefined
user!: Ref<Account>
status!: NotificationStatus
messageId?: Ref<ActivityMessage>
messageClass?: Ref<Class<ActivityMessage>>
objectId!: Ref<Doc>
objectClass!: Ref<Class<Doc>>
}
@Model(notification.class.PushSubscription, core.class.Doc, DOMAIN_USER_NOTIFY)

View File

@ -34,7 +34,6 @@ export default mergeIds(timeId, time, {
EditToDo: '' as IntlString,
GotoTimePlaning: '' as IntlString,
GotoTimeTeamPlaning: '' as IntlString,
NewToDo: '' as IntlString,
Priority: '' as IntlString,
MarkedAsDone: '' as IntlString
},

View File

@ -31,14 +31,15 @@
export let isAsideOpened = false
export let syncLocation = true
export let freeze = false
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
let dataProvider: ChannelDataProvider | undefined
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
const unsubscribe = messageInFocus.subscribe((id) => {
if (!syncLocation) return
if (id !== undefined && id !== selectedMessageId) {
selectedMessageId = id
}
@ -47,9 +48,7 @@
})
const unsubscribeLocation = locationStore.subscribe((newLocation) => {
if (!syncLocation) {
return
}
if (!syncLocation) return
const id = getMessageFromLoc(newLocation)
selectedMessageId = id
messageInFocus.set(id)

View File

@ -582,13 +582,13 @@
}
async function restoreScroll () {
if (!scrollElement || !scroller) {
await wait()
if (!scrollElement || !scroller || scrollToRestore === 0) {
scrollToRestore = 0
return
}
await wait()
const delta = scrollElement.scrollHeight - scrollToRestore
scroller.scrollBy(delta)

View File

@ -18,7 +18,9 @@
import { DocNotifyContext } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { Widget } from '@hcengineering/workbench'
import { ActivityMessage } from '@hcengineering/activity'
import { ChatWidgetTab } from '@hcengineering/chunter'
import { updateTabData } from '@hcengineering/workbench-resources'
import Channel from './Channel.svelte'
import { closeThreadInSidebarChannel } from '../navigation'
@ -36,12 +38,18 @@
let object: Doc | undefined = undefined
let context: DocNotifyContext | undefined = undefined
let selectedMessageId: Ref<ActivityMessage> | undefined = tab.data.selectedMessageId
$: context = object ? $contextByDocStore.get(object._id) : undefined
$: void loadObject(tab.data._id, tab.data._class)
$: threadId = tab.data.thread
$: if (tab.data.selectedMessageId !== undefined && tab.data.selectedMessageId !== '') {
selectedMessageId = tab.data.selectedMessageId
updateTabData(widget._id, tab.id, { selectedMessageId: '' })
}
async function loadObject (_id?: Ref<Doc>, _class?: Ref<Class<Doc>>): Promise<void> {
if (_id === undefined || _class === undefined) {
object = undefined
@ -80,13 +88,19 @@
on:close
/>
{#key object._id}
<Channel {object} {context} syncLocation={false} freeze={threadId !== undefined} />
<Channel {object} {context} syncLocation={false} freeze={threadId !== undefined} {selectedMessageId} />
{/key}
</div>
{/if}
{#if threadId}
<div class="thread" style:height style:width>
<ThreadView _id={threadId} on:channel={() => closeThreadInSidebarChannel(widget, tab)} on:close />
<ThreadView
_id={threadId}
{selectedMessageId}
syncLocation={false}
on:channel={() => closeThreadInSidebarChannel(widget, tab)}
on:close
/>
</div>
{/if}

View File

@ -0,0 +1,79 @@
<script lang="ts">
import activity, { ActivityMessage } from '@hcengineering/activity'
import ThreadParentMessage from './ThreadParentPresenter.svelte'
import { Label } from '@hcengineering/ui'
import ChannelScrollView from '../ChannelScrollView.svelte'
import { Ref } from '@hcengineering/core'
import { ChannelDataProvider } from '../../channelDataProvider'
import chunter from '../../plugin'
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let message: ActivityMessage
let dataProvider: ChannelDataProvider | undefined = undefined
$: if (message !== undefined && dataProvider === undefined) {
dataProvider = new ChannelDataProvider(
undefined,
message.space,
message._id,
chunter.class.ThreadMessage,
selectedMessageId,
true
)
}
$: messagesStore = dataProvider?.messagesStore
</script>
<div class="hulyComponent-content hulyComponent-content__container noShrink">
{#if dataProvider !== undefined}
<ChannelScrollView
bind:selectedMessageId
embedded
skipLabels
object={message}
provider={dataProvider}
fullHeight={false}
fixedInput={false}
>
<svelte:fragment slot="header">
<div class="mt-3">
<ThreadParentMessage {message} />
</div>
{#if (message.replies ?? $messagesStore?.length ?? 0) > 0}
<div class="separator">
<div class="label lower">
<Label
label={activity.string.RepliesCount}
params={{ replies: message.replies ?? $messagesStore?.length ?? 1 }}
/>
</div>
<div class="line" />
</div>
{/if}
</svelte:fragment>
</ChannelScrollView>
{/if}
</div>
<style lang="scss">
.separator {
display: flex;
align-items: center;
margin: 0.5rem 0;
.label {
white-space: nowrap;
margin: 0 0.5rem;
color: var(--theme-halfcontent-color);
}
.line {
background: var(--theme-refinput-border);
height: 1px;
width: 100%;
}
}
</style>

View File

@ -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, BreadcrumbItem } from '@hcengineering/ui'
import { Breadcrumbs, location as locationStore, Header, BreadcrumbItem, Loading } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte'
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import { getMessageFromLoc, messageInFocus } from '@hcengineering/activity-resources'
@ -23,14 +23,14 @@
import attachment from '@hcengineering/attachment'
import chunter from '../../plugin'
import ThreadParentMessage from './ThreadParentPresenter.svelte'
import { getObjectIcon, getChannelName } from '../../utils'
import ChannelScrollView from '../ChannelScrollView.svelte'
import { ChannelDataProvider } from '../../channelDataProvider'
import { threadMessagesStore } from '../../stores'
import ThreadContent from './ThreadContent.svelte'
export let _id: Ref<ActivityMessage>
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let showHeader: boolean = true
export let syncLocation = true
const client = getClient()
const hierarchy = client.getHierarchy()
@ -40,12 +40,12 @@
const channelQuery = createQuery()
let channel: Doc | undefined = undefined
let message: DisplayActivityMessage | undefined = undefined
let message: DisplayActivityMessage | undefined = $threadMessagesStore?._id === _id ? $threadMessagesStore : undefined
let isLoading = true
let channelName: string | undefined = undefined
let dataProvider: ChannelDataProvider | undefined = undefined
const unsubscribe = messageInFocus.subscribe((id) => {
if (!syncLocation) return
if (id !== undefined && id !== selectedMessageId) {
selectedMessageId = id
}
@ -54,6 +54,7 @@
})
const unsubscribeLocation = locationStore.subscribe((newLocation) => {
if (!syncLocation) return
const id = getMessageFromLoc(newLocation)
selectedMessageId = id
messageInFocus.set(id)
@ -64,12 +65,17 @@
unsubscribeLocation()
})
$: if (message && message._id !== _id) {
message = $threadMessagesStore?._id === _id ? $threadMessagesStore : undefined
isLoading = message === undefined
}
$: messageQuery.query(
activity.class.ActivityMessage,
{ _id },
(result: ActivityMessage[]) => {
message = result[0] as DisplayActivityMessage
isLoading = false
if (message === undefined) {
dispatch('close')
}
@ -88,42 +94,26 @@
channel = res[0]
})
$: if (message !== undefined && dataProvider === undefined) {
dataProvider = new ChannelDataProvider(
undefined,
message.space,
message._id,
chunter.class.ThreadMessage,
selectedMessageId,
true
)
}
$: message &&
getChannelName(message.attachedTo, message.attachedToClass, channel).then((res) => {
channelName = res
})
let breadcrumbs: BreadcrumbItem[] = []
$: breadcrumbs = showHeader ? getBreadcrumbsItems(channel, message, channelName) : []
$: breadcrumbs = showHeader ? getBreadcrumbsItems(channel, channelName) : []
function getBreadcrumbsItems (
channel?: Doc,
message?: DisplayActivityMessage,
channelName?: string
): BreadcrumbItem[] {
if (message === undefined) {
function getBreadcrumbsItems (channel?: Doc, channelName?: string): BreadcrumbItem[] {
if (channel === undefined) {
return []
}
const isPersonAvatar =
message.attachedToClass === chunter.class.DirectMessage ||
hierarchy.isDerived(message.attachedToClass, contact.class.Person)
channel._class === chunter.class.DirectMessage || hierarchy.isDerived(channel._class, contact.class.Person)
return [
{
id: 'channel',
icon: getObjectIcon(message.attachedToClass),
icon: getObjectIcon(channel._class),
iconProps: { value: channel },
iconWidth: isPersonAvatar ? 'auto' : undefined,
withoutIconBackground: isPersonAvatar,
@ -143,8 +133,6 @@
dispatch('channel')
}
$: messagesStore = dataProvider?.messagesStore
</script>
{#if showHeader}
@ -153,54 +141,10 @@
</Header>
{/if}
<div class="hulyComponent-content hulyComponent-content__container noShrink">
{#if message && dataProvider !== undefined}
<ChannelScrollView
bind:selectedMessageId
embedded
skipLabels
object={message}
provider={dataProvider}
fullHeight={false}
fixedInput={false}
>
<svelte:fragment slot="header">
<div class="mt-3">
<ThreadParentMessage {message} />
</div>
{#if (message.replies ?? $messagesStore?.length ?? 0) > 0}
<div class="separator">
<div class="label lower">
<Label
label={activity.string.RepliesCount}
params={{ replies: message.replies ?? $messagesStore?.length ?? 1 }}
/>
</div>
<div class="line" />
</div>
{/if}
</svelte:fragment>
</ChannelScrollView>
{/if}
</div>
<style lang="scss">
.separator {
display: flex;
align-items: center;
margin: 0.5rem 0;
.label {
white-space: nowrap;
margin: 0 0.5rem;
color: var(--theme-halfcontent-color);
}
.line {
background: var(--theme-refinput-border);
height: 1px;
width: 100%;
}
}
</style>
{#if message}
{#key _id}
<ThreadContent bind:selectedMessageId {message} />
{/key}
{:else if isLoading}
<Loading />
{/if}

View File

@ -28,6 +28,7 @@ import { get } from 'svelte/store'
import { chatSpecials } from './components/chat/utils'
import { getChannelName, isThreadMessage } from './utils'
import chunter from './plugin'
import { threadMessagesStore } from './stores'
export function openChannel (_id: string, _class: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>): void {
const loc = getCurrentLocation()
@ -168,6 +169,8 @@ export async function replyToThread (message: ActivityMessage, e: Event): Promis
const fromSidebar = isElementFromSidebar(e.target as HTMLElement)
const loc = getCurrentLocation()
threadMessagesStore.set(message)
if (fromSidebar) {
const widget = getClient().getModel().findAllSync(workbench.class.Widget, { _id: chunter.ids.ChatWidget })[0]
const widgetState = get(sidebarStore).widgetsState.get(widget._id)
@ -235,7 +238,8 @@ export async function openChannelInSidebar (
_class: Ref<Class<Doc>>,
doc?: Doc,
thread?: Ref<ActivityMessage>,
newTab = true
newTab = true,
selectedMessageId?: Ref<ActivityMessage>
): Promise<void> {
const client = getClient()
@ -267,6 +271,7 @@ export async function openChannelInSidebar (
_id,
_class,
thread,
selectedMessageId,
channelName: name
}
}

View File

@ -17,11 +17,14 @@ import { writable } from 'svelte/store'
import { type ChatMessage } from '@hcengineering/chunter'
import { type Markup, type Ref } from '@hcengineering/core'
import { languageStore } from '@hcengineering/ui'
import { type ActivityMessage } from '@hcengineering/activity'
export const translatingMessagesStore = writable<Set<Ref<ChatMessage>>>(new Set())
export const translatedMessagesStore = writable<Map<Ref<ChatMessage>, Markup>>(new Map())
export const shownTranslatedMessagesStore = writable<Set<Ref<ChatMessage>>>(new Set())
export const threadMessagesStore = writable<ActivityMessage | undefined>(undefined)
languageStore.subscribe(() => {
translatedMessagesStore.set(new Map())
shownTranslatedMessagesStore.set(new Set())

View File

@ -100,7 +100,13 @@ export interface InlineButton extends AttachedDoc {
}
export interface ChatWidgetTab extends WidgetTab {
data: { _id?: Ref<Doc>, _class?: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>, channelName: string }
data: {
_id?: Ref<Doc>
_class?: Ref<Class<Doc>>
thread?: Ref<ActivityMessage>
channelName: string
selectedMessageId?: Ref<ActivityMessage>
}
}
/**
@ -234,7 +240,14 @@ export default plugin(chunterId, {
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>
(
_id: Ref<Doc>,
_class: Ref<Class<Doc>>,
doc?: Doc,
thread?: Ref<ActivityMessage>,
newTab?: boolean,
selectedMessageId?: Ref<ActivityMessage>
) => Promise<void>
>
}
})

View File

@ -64,6 +64,7 @@
$: if (object !== undefined && object?._id !== value.objectId) {
object = undefined
isLoading = true
}
let isActionMenuOpened = false

View File

@ -1,13 +1,14 @@
<script lang="ts">
import { PersonAccount } from '@hcengineering/contact'
import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import { Class, Doc, Ref } from '@hcengineering/core'
import { BrowserNotification } from '@hcengineering/notification'
import { Button, navigate, Notification as PlatformNotification, NotificationToast } from '@hcengineering/ui'
import view, { decodeObjectURI } from '@hcengineering/view'
import chunter from '@hcengineering/chunter'
import view from '@hcengineering/view'
import chunter, { ThreadMessage } from '@hcengineering/chunter'
import { getResource } from '@hcengineering/platform'
import { ActivityMessage } from '@hcengineering/activity'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { getClient } from '@hcengineering/presentation'
import { pushAvailable, subscribePush } from '../utils'
import plugin from '../plugin'
@ -15,6 +16,9 @@
export let notification: PlatformNotification
export let onRemove: () => void
const client = getClient()
const hierarchy = client.getHierarchy()
$: value = notification.params?.value as BrowserNotification
$: senderAccount =
@ -24,18 +28,35 @@
async function openChannelInSidebar (): Promise<void> {
if (!value.onClickLocation) return
const { onClickLocation } = value
const [_id, _class] = decodeObjectURI(onClickLocation.path[3] ?? '')
let _id: Ref<Doc> | undefined = value.objectId
let _class: Ref<Class<Doc>> | undefined = value.objectClass
let thread = onClickLocation.path[4] as Ref<ActivityMessage> | undefined
const selectedMessageId: Ref<ActivityMessage> | undefined = value.messageId
if (_class && _id && hierarchy.isDerived(_class, activity.class.ActivityMessage)) {
const message = await client.findOne<ActivityMessage>(_class, { _id: _id as Ref<ActivityMessage> })
if (hierarchy.isDerived(_class, chunter.class.ThreadMessage)) {
const threadMessage = message as ThreadMessage
_id = threadMessage?.objectId
_class = threadMessage?.objectClass
thread = threadMessage?.attachedTo
} else {
_id = message?.attachedTo
_class = message?.attachedToClass
thread = (message?.replies ?? 0) > 0 ? message?._id : undefined
}
}
onRemove()
if (!_id || !_class || _id === '' || _class === '') {
if (!_id || !_class || _id === '' || _class === '' || selectedMessageId === undefined) {
navigate(onClickLocation)
return
}
const thread = onClickLocation.path[4] as Ref<ActivityMessage> | undefined
const fn = await getResource(chunter.function.OpenChannelInSidebar)
await fn(_id, _class, undefined, thread)
await fn(_id, _class, undefined, thread, true, selectedMessageId)
}
</script>
@ -56,7 +77,7 @@
<Button
label={view.string.Open}
on:click={() => {
openChannelInSidebar()
void openChannelInSidebar()
}}
/>
{/if}

View File

@ -53,6 +53,10 @@ export interface BrowserNotification extends Doc {
onClickLocation?: Location
senderId?: Ref<Account>
tag: Ref<Doc>
messageId?: Ref<ActivityMessage>
messageClass?: Ref<Class<ActivityMessage>>
objectId: Ref<Doc>
objectClass: Ref<Class<Doc>>
}
export interface PushData {

View File

@ -160,6 +160,7 @@ export default plugin(timeId, {
CreatedToDo: '' as IntlString,
AddToDo: '' as IntlString,
NewToDoDetails: '' as IntlString,
ToDo: '' as IntlString
ToDo: '' as IntlString,
NewToDo: '' as IntlString
}
})

View File

@ -316,3 +316,20 @@ export function minimizeSidebar (closedByUser = false): void {
sidebarStore.set({ ...state, ...widgetsState, widget: undefined, variant: SidebarVariant.MINI })
}
export function updateTabData (widget: Ref<Widget>, tabId: string, data: Record<string, any>): void {
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget)
if (widgetState === undefined) return
const tabs = widgetState.tabs.map((it) => (it.id === tabId ? { ...it, data: { ...it.data, ...data } } : it))
widgetsState.set(widget, { ...widgetState, tabs })
sidebarStore.set({
...state,
widgetsState
})
}

View File

@ -175,7 +175,8 @@ export async function getCommonNotificationTxes (
doc,
receiver,
sender,
subscriptions
subscriptions,
_class
)
}
@ -487,6 +488,14 @@ export async function getTranslatedNotificationContent (
return { title: '', body: '' }
}
function isReactionMessage (message?: ActivityMessage): boolean {
return (
message !== undefined &&
message._class === activity.class.DocUpdateMessage &&
(message as DocUpdateMessage).objectClass === activity.class.Reaction
)
}
export async function createPushFromInbox (
control: TriggerControl,
receiver: ReceiverInfo,
@ -497,10 +506,9 @@ export async function createPushFromInbox (
sender: SenderInfo,
_id: Ref<Doc>,
subscriptions: PushSubscription[],
cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
message?: ActivityMessage
): Promise<Tx | undefined> {
let { title, body } = await getTranslatedNotificationContent(data, _class, control)
if (title === '' || body === '') {
return
}
@ -515,13 +523,12 @@ export async function createPushFromInbox (
if (provider !== undefined) {
const encodeFn = await getResource(provider.encode)
const doc = cache.get(attachedTo) ?? (await control.findAll(control.ctx, attachedToClass, { _id: attachedTo }))[0]
const doc = (await control.findAll(control.ctx, attachedToClass, { _id: attachedTo }))[0]
if (doc === undefined) {
return
}
cache.set(doc._id, doc)
id = await encodeFn(doc, control)
}
@ -543,6 +550,12 @@ export async function createPushFromInbox (
body,
senderId: sender._id,
tag: _id,
objectId: attachedTo,
objectClass: attachedToClass,
messageId: isReactionMessage(message) ? (message?.attachedTo as Ref<ActivityMessage>) : message?._id,
messageClass: isReactionMessage(message)
? (message?.attachedToClass as Ref<Class<ActivityMessage>>)
: message?._class,
onClickLocation: {
path
}
@ -664,6 +677,7 @@ export async function applyNotificationProviders (
receiver: ReceiverInfo,
sender: SenderInfo,
subscriptions: PushSubscription[],
_class = notification.class.ActivityInboxNotification,
message?: ActivityMessage
): Promise<void> {
const resources = control.modelDb.findAllSync(serverNotification.class.NotificationProviderResources, {})
@ -676,10 +690,11 @@ export async function applyNotificationProviders (
attachedTo,
attachedToClass,
data,
notification.class.ActivityInboxNotification,
_class,
sender,
data._id,
subscriptions
subscriptions,
message
)
if (pushTx !== undefined) {
res.push(pushTx)
@ -796,6 +811,7 @@ export async function getNotificationTxes (
receiver,
sender,
subscriptions,
notificationData._class,
message
)
}

View File

@ -172,6 +172,8 @@
"@hcengineering/github": "^0.6.0",
"@hcengineering/github-assets": "^0.6.0",
"@hcengineering/server-ai-bot": "^0.6.0",
"@hcengineering/server-ai-bot-resources": "^0.6.0"
"@hcengineering/server-ai-bot-resources": "^0.6.0",
"@hcengineering/time": "^0.6.0",
"@hcengineering/time-assets": "^0.6.0"
}
}

View File

@ -33,6 +33,7 @@ import { trackerId } from '@hcengineering/tracker'
import { trainingId } from '@hcengineering/training'
import { viewId } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
import { timeId } from '@hcengineering/time'
import coreEng from '@hcengineering/core/lang/en.json'
import loginEng from '@hcengineering/login-assets/lang/en.json'
import platformEng from '@hcengineering/platform/lang/en.json'
@ -68,6 +69,7 @@ import trackerEn from '@hcengineering/tracker-assets/lang/en.json'
import trainingEn from '@hcengineering/training-assets/lang/en.json'
import viewEn from '@hcengineering/view-assets/lang/en.json'
import workbenchEn from '@hcengineering/workbench-assets/lang/en.json'
import timeEn from '@hcengineering/time-assets/lang/en.json'
export function registerStringLoaders (): void {
addStringsLoader(coreId, async (lang: string) => coreEng)
@ -106,4 +108,5 @@ export function registerStringLoaders (): void {
addStringsLoader(productsId, async (lang: string) => productsEn)
addStringsLoader(trainingId, async (lang: string) => trainingEn)
addStringsLoader(githubId, async (lang: string) => githubEn)
addStringsLoader(timeId, async (lang: string) => timeEn)
}