UBERF-8547: Inbox cleanup and other (#7058)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-10-29 17:06:16 +04:00 committed by GitHub
parent 400772b792
commit f258c6755a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 397 additions and 1120 deletions

View File

@ -33,7 +33,8 @@ import {
type Space,
type Timestamp,
type Tx,
type TxCUD
type TxCUD,
DOMAIN_TRANSIENT
} from '@hcengineering/core'
import {
ArrOf,
@ -75,7 +76,6 @@ import {
type NotificationProvider,
type NotificationProviderDefaults,
type NotificationProviderSetting,
type NotificationStatus,
type NotificationTemplate,
type NotificationType,
type NotificationTypeSetting,
@ -92,7 +92,7 @@ export { notificationId, DOMAIN_USER_NOTIFY, DOMAIN_NOTIFICATION, DOMAIN_DOC_NOT
export { notificationOperation } from './migration'
export { notification as default }
@Model(notification.class.BrowserNotification, core.class.Doc, DOMAIN_USER_NOTIFY)
@Model(notification.class.BrowserNotification, core.class.Doc, DOMAIN_TRANSIENT)
export class TBrowserNotification extends TDoc implements BrowserNotification {
senderId?: Ref<Account> | undefined
tag!: Ref<Doc<Space>>
@ -100,7 +100,6 @@ export class TBrowserNotification extends TDoc implements BrowserNotification {
body!: string
onClickLocation?: Location | undefined
user!: Ref<Account>
status!: NotificationStatus
messageId?: Ref<ActivityMessage>
messageClass?: Ref<Class<ActivityMessage>>
objectId!: Ref<Doc>
@ -368,6 +367,10 @@ export function createModel (builder: Builder): void {
TNotificationProviderDefaults
)
builder.mixin(notification.class.BrowserNotification, core.class.Class, core.mixin.TransientConfiguration, {
broadcastOnly: true
})
builder.createDoc(
setting.class.SettingsCategory,
core.space.Model,
@ -389,6 +392,7 @@ export function createModel (builder: Builder): void {
{
label: notification.string.Inbox,
icon: notification.icon.Notifications,
locationDataResolver: notification.function.LocationDataResolver,
alias: notificationId,
hidden: true,
locationResolver: notification.resolver.Location,

View File

@ -23,7 +23,6 @@ import {
} from '@hcengineering/model'
import notification, {
notificationId,
NotificationStatus,
type BrowserNotification,
type DocNotifyContext,
type InboxNotification
@ -401,7 +400,7 @@ export const notificationOperation: MigrateOperation = {
}
},
{
state: 'remove-update-txes-docnotify-ctx',
state: 'remove-update-txes-docnotify-ctx-v2',
func: async (client) => {
await client.deleteMany(DOMAIN_TX, {
_class: core.class.TxUpdateDoc,
@ -410,14 +409,28 @@ export const notificationOperation: MigrateOperation = {
$exists: true
}
})
await client.deleteMany(DOMAIN_TX, {
_class: core.class.TxUpdateDoc,
objectClass: notification.class.DocNotifyContext,
'operations.lastUpdateTimestamp': {
$exists: true
}
})
}
},
{
state: 'remove-browser-notification-v2',
func: async (client) => {
await client.deleteMany<BrowserNotification>(DOMAIN_USER_NOTIFY, {
_class: notification.class.BrowserNotification
})
await client.deleteMany(DOMAIN_TX, {
objectClass: notification.class.BrowserNotification
})
}
}
])
await client.deleteMany<BrowserNotification>(DOMAIN_USER_NOTIFY, {
_class: notification.class.BrowserNotification,
status: NotificationStatus.Notified
})
},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {}
}

View File

@ -17,9 +17,9 @@
import { type Doc, type Ref } from '@hcengineering/core'
import notification, { notificationId } from '@hcengineering/notification'
import { type IntlString, type Resource, mergeIds } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import { type AnyComponent, type Location } from '@hcengineering/ui/src/types'
import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view'
import { type Application } from '@hcengineering/workbench'
import { type Application, type LocationData } from '@hcengineering/workbench'
import { type DocUpdateMessageViewlet } from '@hcengineering/activity'
export default mergeIds(notificationId, notification, {
@ -53,7 +53,8 @@ export default mergeIds(notificationId, notification, {
HasDocNotifyContextPinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasDocNotifyContextUnpinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanReadNotifyContext: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanUnReadNotifyContext: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
CanUnReadNotifyContext: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
LocationDataResolver: '' as Resource<(loc: Location) => Promise<LocationData>>
},
category: {
Notification: '' as Ref<ActionCategory>

View File

@ -20,6 +20,7 @@ import {
} from '@hcengineering/model'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import workbench from '@hcengineering/workbench'
import core, { DOMAIN_TX } from '@hcengineering/core'
import { workbenchId } from '.'
@ -33,6 +34,15 @@ export const workbenchOperation: MigrateOperation = {
{
state: 'remove-wrong-tabs-v1',
func: removeTabs
},
{
state: 'remove-txes-update-tabs-v1',
func: async () => {
await client.deleteMany(DOMAIN_TX, {
objectClass: workbench.class.WorkbenchTab,
_class: core.class.TxUpdateDoc
})
}
}
])
},

View File

@ -475,7 +475,6 @@ describe('query', () => {
message: 'child'
}
)
let attempt = 0
const pp = new Promise((resolve) => {
liveQuery.query<AttachedComment>(
test.class.TestComment,
@ -483,14 +482,10 @@ describe('query', () => {
(result) => {
const comment = result[0]
if (comment !== undefined) {
if (attempt++ > 0) {
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space?._id).toEqual(
futureSpace._id
)
resolve(null)
} else {
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
}
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space?._id).toEqual(
futureSpace._id
)
resolve(null)
}
},
{ lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } }
@ -521,7 +516,6 @@ describe('query', () => {
message: 'test'
}
)
let attempt = 0
const childLength = 3
const pp = new Promise((resolve) => {
liveQuery.query<AttachedComment>(
@ -529,10 +523,13 @@ describe('query', () => {
{ _id: parentComment },
(result) => {
const comment = result[0]
if (comment !== undefined) {
expect((comment.$lookup as any)?.comments).toHaveLength(attempt++)
const res = (comment.$lookup as any)?.comments?.length
if (res !== undefined) {
expect(res).toBeGreaterThanOrEqual(1)
expect(res).toBeLessThanOrEqual(childLength)
}
if (attempt === childLength) {
if ((res ?? 0) === childLength) {
resolve(null)
}
},
@ -628,23 +625,15 @@ describe('query', () => {
message: 'child'
}
)
let attempt = -1
const pp = new Promise((resolve) => {
liveQuery.query<AttachedComment>(
test.class.TestComment,
{ _id: childComment },
(result) => {
attempt++
const comment = result[0]
if (comment !== undefined) {
if (attempt > 0) {
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
resolve(null)
} else {
expect(
((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space as Doc)?._id
).toEqual(futureSpace)
}
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
resolve(null)
}
},
{ lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } }

View File

@ -95,6 +95,8 @@ export class LiveQuery implements WithTx, Client {
private queryCounter: number = 0
private closed: boolean = false
private readonly queriesToUpdate = new Map<number, [Query, Doc[]]>()
// A map of _class to documents.
private readonly documentRefs = new Map<string, Map<Ref<Doc>, DocumentRef>>()
@ -773,7 +775,7 @@ export class LiveQuery implements WithTx, Client {
if (q.options?.sort !== undefined) {
await resultSort(q.result, q.options?.sort, q._class, this.getHierarchy(), this.client.getModel())
}
await this.callback(q)
await this.callback(q, true)
}
}
@ -846,6 +848,7 @@ export class LiveQuery implements WithTx, Client {
}
private async refresh (q: Query): Promise<void> {
this.queriesToUpdate.delete(q.id)
await q.refresh()
}
@ -1040,10 +1043,10 @@ export class LiveQuery implements WithTx, Client {
if (q.options?.limit !== undefined && q.result.length > q.options.limit) {
if (q.result.pop()?._id !== doc._id || q.options?.total === true) {
await this.callback(q)
await this.callback(q, true)
}
} else {
await this.callback(q)
await this.callback(q, true)
}
}
}
@ -1051,7 +1054,7 @@ export class LiveQuery implements WithTx, Client {
await this.handleDocAddLookup(q, doc)
}
private async callback (q: Query): Promise<void> {
private async callback (q: Query, bulkUpdate = false): Promise<void> {
if (q.result instanceof Promise) {
q.result = await q.result
}
@ -1059,9 +1062,15 @@ export class LiveQuery implements WithTx, Client {
this.updateDocuments(q, q.result)
const result = q.result
Array.from(q.callbacks.values()).forEach((callback) => {
callback(toFindResult(this.clone(result), q.total))
})
if (bulkUpdate) {
this.queriesToUpdate.set(q.id, [q, result])
} else {
this.queriesToUpdate.delete(q.id)
Array.from(q.callbacks.values()).forEach((callback) => {
callback(toFindResult(this.clone(result), q.total))
})
}
}
private updateDocuments (q: Query, docs: Doc[], clean: boolean = false): void {
@ -1106,7 +1115,7 @@ export class LiveQuery implements WithTx, Client {
if (q.options?.sort !== undefined) {
await resultSort(q.result, q.options?.sort, q._class, this.getHierarchy(), this.getModel())
}
await this.callback(q)
await this.callback(q, true)
}
}
@ -1181,7 +1190,7 @@ export class LiveQuery implements WithTx, Client {
if (q.options?.total === true) {
q.total--
}
await this.callback(q)
await this.callback(q, true)
}
await this.handleDocRemoveLookup(q, tx)
}
@ -1220,7 +1229,7 @@ export class LiveQuery implements WithTx, Client {
if (q.options?.sort !== undefined) {
await resultSort(q.result, q.options?.sort, q._class, this.getHierarchy(), this.getModel())
}
await this.callback(q)
await this.callback(q, true)
}
}
@ -1294,6 +1303,17 @@ export class LiveQuery implements WithTx, Client {
}
result.push(await this._tx(tx, docCache))
}
if (this.queriesToUpdate.size > 0) {
const copy = new Map(this.queriesToUpdate)
this.queriesToUpdate.clear()
for (const [q, res] of copy.values()) {
Array.from(q.callbacks.values()).forEach((callback) => {
callback(toFindResult(this.clone(res), q.total))
})
}
}
return result
}
@ -1533,10 +1553,10 @@ export class LiveQuery implements WithTx, Client {
return
}
if (q.result.pop()?._id !== updatedDoc._id) {
await this.callback(q)
await this.callback(q, true)
}
} else {
await this.callback(q)
await this.callback(q, true)
}
}
}

View File

@ -59,11 +59,11 @@
margin: 0;
}
&.default.highlighted:hover {
&.default.highlighted:hover:not(.highlighted) {
background-color: var(--theme-popup-divider);
}
&.lumia.highlighted:hover {
&.lumia.highlighted:hover:not(.highlighted) {
background-color: var(--global-ui-highlight-BackgroundColor);
}

View File

@ -5,6 +5,7 @@ import { type AnyComponent, type AnySvelteComponent } from '../../types'
export interface Notification {
id: string
title: string
group?: string
component: AnyComponent | AnySvelteComponent
subTitle?: string
subTitlePostfix?: string

View File

@ -18,11 +18,19 @@ export const addNotification = (notification: Notification, store: Writable<Noti
id: generateId()
}
update((notifications: Notification[]) =>
[NotificationPosition.TopRight, NotificationPosition.TopLeft].includes(newNotification.position)
update((notifications: Notification[]) => {
if (
notification.group != null &&
notification.group !== '' &&
notifications.some(({ group }) => group === notification.group)
) {
return notifications.map((n) => (n.group === notification.group ? newNotification : n))
}
return [NotificationPosition.TopRight, NotificationPosition.TopLeft].includes(newNotification.position)
? [newNotification, ...notifications]
: [...notifications, newNotification]
)
})
}
export const removeNotification = (notificationId: string, { update }: Writable<Notification[]>): void => {

View File

@ -103,7 +103,8 @@ export function addNotification (
subTitle: string,
component: AnyComponent | AnySvelteComponent,
params?: Record<string, any>,
severity: NotificationSeverity = NotificationSeverity.Success
severity: NotificationSeverity = NotificationSeverity.Success,
group?: string
): void {
const closeTimeout = parseInt(localStorage.getItem('#platform.notification.timeout') ?? '10000')
const notification: Notification = {
@ -111,6 +112,7 @@ export function addNotification (
title,
subTitle,
severity,
group,
position: NotificationPosition.BottomLeft,
component,
closeTimeout,

View File

@ -30,6 +30,7 @@
export let filters: Ref<ActivityMessagesFilter>[] = []
export let isAsideOpened = false
export let syncLocation = true
export let autofocus = true
export let freeze = false
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
@ -108,6 +109,7 @@
{collection}
provider={dataProvider}
{freeze}
{autofocus}
loadMoreAllowed={!isDocChannel}
/>
{/if}

View File

@ -29,6 +29,7 @@
export let boundary: HTMLElement | undefined | null = undefined
export let collection: string | undefined
export let isThread = false
export let autofocus = true
const client = getClient()
const hierarchy = client.getHierarchy()
@ -68,7 +69,7 @@
<ActivityExtensionComponent
kind="input"
{extensions}
props={{ object, boundary, collection, autofocus: true, withTypingInfo: true }}
props={{ object, boundary, collection, autofocus, withTypingInfo: true }}
/>
</div>
{:else}

View File

@ -22,6 +22,7 @@
export let _id: Ref<ChunterSpace>
export let _class: Ref<Class<ChunterSpace>>
export let autofocus = true
const objectQuery = createQuery()
const inboxClient = InboxNotificationsClientImpl.getClient()
@ -37,5 +38,5 @@
</script>
{#if object}
<ChannelView {object} {context} on:close />
<ChannelView {object} {context} {autofocus} on:close />
{/if}

View File

@ -1,917 +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 activity, { ActivityMessage, ActivityMessagesFilter, DisplayActivityMessage } from '@hcengineering/activity'
import {
ActivityMessagePresenter,
canGroupMessages,
messageInFocus,
sortActivityMessages
} from '@hcengineering/activity-resources'
import core, { Doc, getCurrentAccount, getDay, Ref, Space, Timestamp } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Loading, ModernButton, Scroller, ScrollParams } from '@hcengineering/ui'
import { afterUpdate, beforeUpdate, onDestroy, onMount, tick } from 'svelte'
import { get } from 'svelte/store'
import { ChannelDataProvider, MessageMetadata } from '../channelDataProvider'
import chunter from '../plugin'
import {
chatReadMessagesStore,
filterChatMessages,
getClosestDate,
readChannelMessages,
recheckNotifications
} from '../utils'
import BlankView from './BlankView.svelte'
import ActivityMessagesSeparator from './ChannelMessagesSeparator.svelte'
import JumpToDateSelector from './JumpToDateSelector.svelte'
import HistoryLoading from './LoadingHistory.svelte'
import ChannelInput from './ChannelInput.svelte'
import { messageInView } from '../scroll'
export let provider: ChannelDataProvider
export let object: Doc
export let channel: Doc
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let scrollElement: HTMLDivElement | undefined | null = undefined
export let startFromBottom = false
export let selectedFilters: Ref<ActivityMessagesFilter>[] = []
export let embedded = false
export let collection: string | undefined = undefined
export let showEmbedded = false
export let skipLabels = false
export let loadMoreAllowed = true
export let isAsideOpened = false
export let fullHeight = true
export let fixedInput = true
export let freeze = false
const doc = object
const dateSelectorHeight = 30
const headerHeight = 52
const minMsgHeightRem = 2
const loadMoreThreshold = 40
const me = getCurrentAccount()
const client = getClient()
const hierarchy = client.getHierarchy()
const inboxClient = InboxNotificationsClientImpl.getClient()
const contextByDocStore = inboxClient.contextByDoc
const notificationsByContextStore = inboxClient.inboxNotificationsByContext
let filters: ActivityMessagesFilter[] = []
const filterResources = new Map<
Ref<ActivityMessagesFilter>,
(message: ActivityMessage, _class?: Ref<Doc>) => boolean
>()
const messagesStore = provider.messagesStore
const isLoadingStore = provider.isLoadingStore
const isLoadingMoreStore = provider.isLoadingMoreStore
const isTailLoadedStore = provider.isTailLoaded
const newTimestampStore = provider.newTimestampStore
const datesStore = provider.datesStore
const metadataStore = provider.metadataStore
let messages: ActivityMessage[] = []
let displayMessages: DisplayActivityMessage[] = []
let scroller: Scroller | undefined | null = undefined
let separatorElement: HTMLDivElement | undefined = undefined
let scrollContentBox: HTMLDivElement | undefined = undefined
let autoscroll = false
let shouldScrollToNew = false
let shouldWaitAndRead = false
let isScrollInitialized = false
let selectedDate: Timestamp | undefined = undefined
let dateToJump: Timestamp | undefined = undefined
let prevScrollHeight = 0
let isScrollAtBottom = false
let messagesCount = 0
let wasAsideOpened = isAsideOpened
$: messages = $messagesStore
$: isLoading = $isLoadingStore
$: notifyContext = $contextByDocStore.get(doc._id)
$: readonly = hierarchy.isDerived(channel._class, core.class.Space) ? (channel as Space).archived : false
void client
.getModel()
.findAll(activity.class.ActivityMessagesFilter, {})
.then(async (res) => {
filters = res
for (const filter of filters) {
filterResources.set(filter._id, await getResource(filter.filter))
}
})
let isPageHidden = false
let lastMsgBeforeFreeze: Ref<ActivityMessage> | undefined = undefined
function handleVisibilityChange (): void {
if (document.hidden) {
isPageHidden = true
lastMsgBeforeFreeze = shouldScrollToNew ? displayMessages[displayMessages.length - 1]?._id : undefined
} else {
if (isPageHidden) {
isPageHidden = false
void provider.updateNewTimestamp(notifyContext)
}
}
}
function isFreeze (): boolean {
return freeze || isPageHidden
}
$: displayMessages = filterChatMessages(messages, filters, filterResources, doc._class, selectedFilters)
const unsubscribe = inboxClient.inboxNotificationsByContext.subscribe(() => {
if (notifyContext !== undefined && !isFreeze()) {
recheckNotifications(notifyContext)
readViewportMessages()
}
})
function scrollToBottom (afterScrollFn?: () => void): void {
if (scroller != null && scrollElement != null && !isFreeze()) {
scroller.scrollBy(scrollElement.scrollHeight)
updateSelectedDate()
afterScrollFn?.()
}
}
function scrollToSeparator (): void {
if (separatorElement && scrollElement) {
const messagesElements = scrollContentBox?.getElementsByClassName('activityMessage')
const messagesHeight = displayMessages
.slice(separatorIndex)
.reduce((res, msg) => res + (messagesElements?.[msg._id as any]?.clientHeight ?? 0), 0)
separatorElement.scrollIntoView()
if (messagesHeight >= scrollElement.clientHeight) {
scroller?.scrollBy(-50)
}
updateShouldScrollToNew()
readViewportMessages()
}
}
function scrollToMessage () {
if (!selectedMessageId) {
return
}
if (!scrollElement || !scrollContentBox) {
setTimeout(scrollToMessage, 50)
}
const messagesElements = scrollContentBox?.getElementsByClassName('activityMessage')
const msgElement = messagesElements?.[selectedMessageId as any]
if (!msgElement) {
if (displayMessages.some(({ _id }) => _id === selectedMessageId)) {
setTimeout(scrollToMessage, 50)
}
return
}
msgElement.scrollIntoView()
readViewportMessages()
}
function isDateRendered (date: Timestamp): boolean {
const day = getDay(date)
return document.getElementById(day.toString()) != null
}
function jumpToDate (e: CustomEvent): void {
const date = e.detail.date
if (!date || !scrollElement) {
return
}
const closestDate = getClosestDate(date, get(provider.datesStore))
if (closestDate === undefined) {
return
}
if (isDateRendered(closestDate)) {
scrollToDate(closestDate)
} else {
void provider.jumpToDate(closestDate)
dateToJump = closestDate
}
}
function scrollToDate (date: Timestamp): void {
autoscroll = false
dateToJump = undefined
shouldWaitAndRead = false
const day = getDay(date)
const element = document.getElementById(day.toString())
let offset = element?.offsetTop
if (!offset || !scroller) {
return
}
offset = offset - headerHeight - dateSelectorHeight / 2
scroller?.scroll(offset)
}
function updateShouldScrollToNew (): void {
if (scrollElement) {
const { offsetHeight, scrollHeight, scrollTop } = scrollElement
const offset = 100
shouldScrollToNew = scrollHeight <= scrollTop + offsetHeight + offset
}
}
function shouldLoadMoreUp (): boolean {
if (!scrollElement) {
return false
}
return scrollElement.scrollTop <= loadMoreThreshold
}
function shouldLoadMoreDown (): boolean {
if (!scrollElement) {
return false
}
const { scrollHeight, scrollTop, clientHeight } = scrollElement
return scrollHeight - Math.ceil(scrollTop + clientHeight) <= loadMoreThreshold
}
let scrollToRestore = 0
let backwardRequested = false
function loadMore (): void {
if (!loadMoreAllowed || $isLoadingMoreStore || !scrollElement || isInitialScrolling) {
return
}
const minMsgHeightPx = minMsgHeightRem * parseFloat(getComputedStyle(document.documentElement).fontSize)
const maxMsgPerScreen = Math.ceil(scrollElement.clientHeight / minMsgHeightPx)
const limit = Math.max(maxMsgPerScreen, provider.limit)
if (!shouldLoadMoreUp()) {
backwardRequested = false
}
if (shouldLoadMoreUp() && !backwardRequested) {
shouldScrollToNew = false
scrollToRestore = scrollElement?.scrollHeight ?? 0
provider.addNextChunk('backward', messages[0]?.createdOn, limit)
backwardRequested = true
} else if (shouldLoadMoreDown() && !$isTailLoadedStore) {
scrollToRestore = 0
shouldScrollToNew = false
isScrollAtBottom = false
provider.addNextChunk('forward', messages[messages.length - 1]?.createdOn, limit)
}
}
function scrollToStartOfNew (): void {
if (!scrollElement || !lastMsgBeforeFreeze) {
return
}
const lastIndex = displayMessages.findIndex(({ _id }) => _id === lastMsgBeforeFreeze)
if (lastIndex === -1) return
const firstNewMessage = displayMessages.find(({ createdBy }, index) => index > lastIndex && createdBy !== me._id)
if (firstNewMessage === undefined) {
scrollToBottom()
return
}
const messagesElements = scrollContentBox?.getElementsByClassName('activityMessage')
const msgElement = messagesElements?.[firstNewMessage._id as any]
if (!msgElement) {
return
}
const messageRect = msgElement.getBoundingClientRect()
const topOffset = messageRect.top - 150
if (topOffset < 0) {
scroller?.scrollBy(topOffset)
} else if (topOffset > 0) {
scroller?.scrollBy(topOffset)
}
}
async function handleScroll ({ autoScrolling }: ScrollParams): Promise<void> {
saveScrollPosition()
updateDownButtonVisibility($metadataStore, displayMessages, scrollElement)
if (autoScrolling) {
return
}
shouldWaitAndRead = false
autoscroll = false
updateShouldScrollToNew()
loadMore()
updateSelectedDate()
readViewportMessages()
}
function isLastMessageViewed (): boolean {
if (!scrollElement) {
return false
}
const last = displayMessages[displayMessages.length - 1]
if (last === undefined) {
return false
}
const containerRect = scrollElement.getBoundingClientRect()
const messagesElements = scrollContentBox?.getElementsByClassName('activityMessage')
const msgElement = messagesElements?.[last._id as any]
if (!msgElement) {
return false
}
return messageInView(msgElement, containerRect)
}
const messagesToReadAccumulator: Set<DisplayActivityMessage> = new Set<DisplayActivityMessage>()
let messagesToReadAccumulatorTimer: any
function readViewportMessages (): void {
if (!scrollElement || !scrollContentBox || isFreeze()) {
return
}
const containerRect = scrollElement.getBoundingClientRect()
const messagesElements = scrollContentBox?.getElementsByClassName('activityMessage')
for (const message of displayMessages) {
const msgElement = messagesElements?.[message._id as any]
if (!msgElement) {
continue
}
if (messageInView(msgElement, containerRect)) {
messagesToReadAccumulator.add(message)
}
}
clearTimeout(messagesToReadAccumulatorTimer)
messagesToReadAccumulatorTimer = setTimeout(() => {
const messagesToRead = [...messagesToReadAccumulator]
messagesToReadAccumulator.clear()
void readChannelMessages(sortActivityMessages(messagesToRead), notifyContext)
}, 500)
}
function updateSelectedDate (): void {
if (embedded) {
return
}
if (!scrollContentBox || !scrollElement) {
return
}
const containerRect = scrollElement.getBoundingClientRect()
const messagesElements = scrollContentBox?.getElementsByClassName('activityMessage')
if (messagesElements === undefined) {
return
}
const reversedDates = [...get(datesStore)].reverse()
for (const message of displayMessages) {
const msgElement = messagesElements?.[message._id as any]
if (!msgElement) {
continue
}
const createdOn = message.createdOn
if (createdOn === undefined) {
continue
}
const messageRect = msgElement.getBoundingClientRect()
const isInView =
messageRect.top > 0 &&
messageRect.top < containerRect.bottom &&
messageRect.bottom - headerHeight - 2 * dateSelectorHeight > 0 &&
messageRect.bottom <= containerRect.bottom
if (isInView) {
selectedDate = reversedDates.find((date) => date <= createdOn)
break
}
}
if (selectedDate) {
const day = getDay(selectedDate)
const dateElement = document.getElementById(day.toString())
let isElementVisible = false
if (dateElement) {
const elementRect = dateElement.getBoundingClientRect()
isElementVisible =
elementRect.top + dateSelectorHeight / 2 >= containerRect.top && elementRect.bottom <= containerRect.bottom
}
if (isElementVisible) {
selectedDate = undefined
}
}
}
$: newTimestamp = $newTimestampStore
$: separatorIndex =
newTimestamp !== undefined
? displayMessages.findIndex((message) => (message.createdOn ?? 0) >= (newTimestamp ?? 0))
: -1
$: void initializeScroll(isLoading, separatorElement, separatorIndex)
let isInitialScrolling = true
async function initializeScroll (isLoading: boolean, separatorElement?: HTMLDivElement, separatorIndex?: number) {
if (isLoading || isScrollInitialized) {
return
}
updateSelectedDate()
if (selectedMessageId !== undefined && messages.some(({ _id }) => _id === selectedMessageId)) {
await wait()
scrollToMessage()
isScrollInitialized = true
isInitialScrolling = false
} else if (separatorIndex === -1) {
await wait()
isScrollInitialized = true
shouldWaitAndRead = true
autoscroll = true
shouldScrollToNew = true
isInitialScrolling = false
waitLastMessageRenderAndRead(() => {
autoscroll = false
})
} else if (separatorElement) {
await wait()
scrollToSeparator()
isScrollInitialized = true
isInitialScrolling = false
}
updateDownButtonVisibility($metadataStore, displayMessages, scrollElement)
}
function reinitializeScroll (): void {
isScrollInitialized = false
void initializeScroll(isLoading, separatorElement, separatorIndex)
}
function adjustScrollPosition (selectedMessageId: Ref<ActivityMessage> | undefined): void {
if (isLoading || !isScrollInitialized || isInitialScrolling) {
return
}
const msg = $metadataStore.find(({ _id }) => _id === selectedMessageId)
if (msg !== undefined) {
const isReload = provider.jumpToMessage(msg)
if (isReload) {
reinitializeScroll()
}
} else if (selectedMessageId === undefined) {
provider.jumpToEnd()
reinitializeScroll()
}
}
$: adjustScrollPosition(selectedMessageId)
function waitLastMessageRenderAndRead (onComplete?: () => void) {
if (isLastMessageViewed()) {
readViewportMessages()
shouldScrollToNew = true
shouldWaitAndRead = false
onComplete?.()
} else if (shouldWaitAndRead && messages.length > 0) {
shouldWaitAndRead = false
setTimeout(() => {
waitLastMessageRenderAndRead(onComplete)
}, 500)
} else {
onComplete?.()
}
}
function scrollToNewMessages (): void {
if (!scrollElement || !shouldScrollToNew) {
readViewportMessages()
return
}
scrollToBottom()
readViewportMessages()
}
async function wait (): Promise<void> {
// One tick is not enough for messages to be rendered,
// I think this is due to the fact that we are using a Component, which takes some time to load,
// because after one tick I see spinners from Component
await tick() // wait until the DOM is updated
await tick() // wait until the DOM is updated
}
async function restoreScroll () {
await wait()
if (!scrollElement || !scroller || scrollToRestore === 0) {
scrollToRestore = 0
return
}
const delta = scrollElement.scrollHeight - scrollToRestore
scroller.scrollBy(delta)
scrollToRestore = 0
dateToJump = 0
autoscroll = false
shouldWaitAndRead = false
}
async function handleMessagesUpdated (newCount: number): Promise<void> {
if (newCount === messagesCount) {
return
}
const prevCount = messagesCount
messagesCount = newCount
if (isFreeze()) {
await wait()
scrollToStartOfNew()
return
}
if (scrollToRestore > 0) {
void restoreScroll()
} else if (dateToJump !== undefined) {
await wait()
scrollToDate(dateToJump)
} else if (shouldScrollToNew && prevCount > 0 && newCount > prevCount) {
await wait()
scrollToNewMessages()
} else {
await wait()
readViewportMessages()
}
}
$: void handleMessagesUpdated(displayMessages.length)
function handleResize (): void {
if (isInitialScrolling || !isScrollInitialized) {
return
}
if (shouldScrollToNew) {
scrollToBottom()
}
loadMore()
}
let timer: any
function saveScrollPosition (): void {
if (!scrollElement) {
return
}
prevScrollHeight = scrollElement.scrollHeight
clearTimeout(timer)
setTimeout(() => {
if (!scrollElement) {
return
}
const { offsetHeight, scrollHeight, scrollTop } = scrollElement
isScrollAtBottom = scrollHeight <= Math.ceil(scrollTop + offsetHeight)
}, 15)
}
beforeUpdate(() => {
if (!scrollElement) return
if (isScrollInitialized && scrollElement.scrollHeight === scrollElement.clientHeight) {
isScrollAtBottom = true
}
})
afterUpdate(() => {
if (!scrollElement) return
const { offsetHeight, scrollHeight, scrollTop } = scrollElement
if (!isInitialScrolling && !isFreeze() && prevScrollHeight < scrollHeight && isScrollAtBottom) {
scrollToBottom()
} else if (isFreeze()) {
isScrollAtBottom = scrollHeight <= Math.ceil(scrollTop + offsetHeight)
}
})
async function compensateAside (isOpened: boolean): Promise<void> {
if (!isInitialScrolling && isScrollAtBottom && !wasAsideOpened && isOpened) {
await wait()
scrollToBottom()
}
wasAsideOpened = isOpened
}
$: void compensateAside(isAsideOpened)
function canGroupChatMessages (message: ActivityMessage, prevMessage?: ActivityMessage): boolean {
let prevMetadata: MessageMetadata | undefined = undefined
if (prevMessage === undefined) {
const metadata = $metadataStore
prevMetadata = metadata.find((_, index) => metadata[index + 1]?._id === message._id)
}
return canGroupMessages(message, prevMessage ?? prevMetadata)
}
onMount(() => {
chatReadMessagesStore.update(() => new Set())
document.addEventListener('visibilitychange', handleVisibilityChange)
})
onDestroy(() => {
unsubscribe()
document.removeEventListener('visibilitychange', handleVisibilityChange)
})
let showScrollDownButton = false
$: updateDownButtonVisibility($metadataStore, displayMessages, scrollElement)
function updateDownButtonVisibility (
metadata: MessageMetadata[],
displayMessages: DisplayActivityMessage[],
element?: HTMLDivElement | null
): void {
if (metadata.length === 0 || displayMessages.length === 0) {
showScrollDownButton = false
return
}
if (!$isTailLoadedStore) {
showScrollDownButton = true
} else if (element != null) {
const { scrollHeight, scrollTop, offsetHeight } = element
showScrollDownButton = scrollHeight > offsetHeight + scrollTop + 50
} else {
showScrollDownButton = false
}
}
async function handleScrollDown (): Promise<void> {
selectedMessageId = undefined
messageInFocus.set(undefined)
const metadata = $metadataStore
const lastMetadata = metadata[metadata.length - 1]
const lastMessage = displayMessages[displayMessages.length - 1]
if (lastMetadata._id !== lastMessage._id) {
separatorIndex = -1
provider.jumpToEnd(true)
reinitializeScroll()
} else {
scrollToBottom()
}
const op = client.apply(undefined, 'chunter.scrollDown')
await inboxClient.readDoc(op, doc._id)
await op.commit()
}
let forceRead = false
$: void forceReadContext(isScrollAtBottom, notifyContext)
async function forceReadContext (isScrollAtBottom: boolean, context?: DocNotifyContext): Promise<void> {
if (context === undefined || !isScrollAtBottom || forceRead || isFreeze()) return
const { lastUpdateTimestamp = 0, lastViewedTimestamp = 0 } = context
if (lastViewedTimestamp >= lastUpdateTimestamp) return
const notifications = $notificationsByContextStore.get(context._id) ?? []
const unViewed = notifications.filter(({ isViewed }) => !isViewed)
if (unViewed.length === 0) {
forceRead = true
const op = client.apply(undefined, 'chunter.forceReadContext', true)
await inboxClient.readDoc(op, object._id)
await op.commit()
}
}
const canLoadNextForwardStore = provider.canLoadNextForwardStore
$: if (!freeze && !isPageHidden && isScrollInitialized) {
readViewportMessages()
}
</script>
{#if isLoading}
<Loading />
{:else}
<div class="flex-col relative" class:h-full={fullHeight}>
{#if startFromBottom}
<div class="grower" />
{/if}
{#if !embedded && displayMessages.length > 0 && selectedDate}
<div class="selectedDate">
<JumpToDateSelector {selectedDate} fixed on:jumpToDate={jumpToDate} />
</div>
{/if}
{#if isInitialScrolling}
<div class="overlay">
<Loading />
</div>
{/if}
<Scroller
{autoscroll}
bottomStart={startFromBottom}
bind:this={scroller}
bind:divScroll={scrollElement}
bind:divBox={scrollContentBox}
noStretch={false}
disableOverscroll
horizontal={false}
onScroll={handleScroll}
onResize={handleResize}
>
{#if loadMoreAllowed && !embedded}
<HistoryLoading isLoading={$isLoadingMoreStore} />
{/if}
<slot name="header" />
{#if displayMessages.length === 0 && !embedded && !readonly}
<BlankView
icon={chunter.icon.Thread}
header={chunter.string.NoMessagesInChannel}
label={chunter.string.SendMessagesInChannel}
/>
{/if}
{#each displayMessages as message, index (message._id)}
{@const isSelected = message._id === selectedMessageId}
{@const canGroup = canGroupChatMessages(message, displayMessages[index - 1])}
{#if separatorIndex === index}
<ActivityMessagesSeparator bind:element={separatorElement} label={activity.string.New} />
{/if}
{#if !embedded && message.createdOn && $datesStore.includes(message.createdOn)}
<JumpToDateSelector selectedDate={message.createdOn} on:jumpToDate={jumpToDate} />
{/if}
<ActivityMessagePresenter
{doc}
value={message}
skipLabel={skipLabels}
{showEmbedded}
hoverStyles="filledHover"
isHighlighted={isSelected}
shouldScroll={isSelected}
withShowMore={false}
attachmentImageSize="x-large"
type={canGroup ? 'short' : 'default'}
hideLink
{readonly}
/>
{/each}
{#if !fixedInput}
<ChannelInput {object} {readonly} boundary={scrollElement} {collection} isThread={embedded} />
{/if}
{#if loadMoreAllowed && $canLoadNextForwardStore}
<HistoryLoading isLoading={$isLoadingMoreStore} />
{/if}
</Scroller>
{#if !embedded && showScrollDownButton}
<div class="down-button absolute" class:readonly>
<ModernButton
label={chunter.string.LatestMessages}
shape="round"
size="small"
kind="primary"
on:click={handleScrollDown}
/>
</div>
{/if}
</div>
{#if fixedInput}
<ChannelInput {object} {readonly} boundary={scrollElement} {collection} isThread={embedded} />
{/if}
{/if}
<style lang="scss">
.grower {
flex-grow: 10;
flex-shrink: 5;
}
.overlay {
width: 100%;
height: 100%;
position: absolute;
background: var(--theme-panel-color);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
}
.selectedDate {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.down-button {
width: 100%;
display: flex;
justify-content: center;
bottom: -0.75rem;
animation: 0.5s fadeIn;
animation-fill-mode: forwards;
visibility: hidden;
&.readonly {
bottom: 0.25rem;
}
}
@keyframes fadeIn {
99% {
visibility: hidden;
}
100% {
visibility: visible;
}
}
</style>

View File

@ -97,6 +97,7 @@
{#if threadId && visible}
<div class="thread" style:height style:width>
<ThreadView
{...tab.data.props}
_id={threadId}
{selectedMessageId}
syncLocation={false}

View File

@ -41,6 +41,7 @@
export let object: Doc
export let context: DocNotifyContext | undefined
export let autofocus = true
export let embedded: boolean = false
const client = getClient()
@ -143,6 +144,7 @@
{context}
{object}
{filters}
{autofocus}
isAsideOpened={(withAside && isAsideShown) || isThreadOpened}
/>
{/if}

View File

@ -52,6 +52,7 @@
export let fullHeight = true
export let freeze = false
export let loadMoreAllowed = true
export let autofocus = true
const minMsgHeightRem = 2
const loadMoreThreshold = 200
@ -400,9 +401,7 @@
scrollToBottom()
}
const op = client.apply(undefined, 'chunter.scrollDown')
await inboxClient.readDoc(op, doc._id)
await op.commit()
await inboxClient.readDoc(doc._id)
}
let forceRead = false
@ -419,9 +418,7 @@
if (unViewed.length === 0) {
forceRead = true
const op = client.apply(undefined, 'chunter.forceReadContext', true)
await inboxClient.readDoc(op, object._id)
await op.commit()
await inboxClient.readDoc(object._id)
}
}
@ -642,7 +639,7 @@
<HistoryLoading isLoading={$isLoadingMoreStore} />
{/if}
{#if !fixedInput}
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} />
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} {autofocus} />
{/if}
</BaseChatScroller>
{#if !isThread && isLatestMessageButtonVisible}
@ -659,7 +656,7 @@
</div>
{#if fixedInput}
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} />
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} {autofocus} />
{/if}
<style lang="scss">

View File

@ -13,6 +13,7 @@
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let message: ActivityMessage
export let autofocus = true
const client = getClient()
const hierarchy = client.getHierarchy()
@ -65,6 +66,7 @@
object={message}
{channel}
provider={dataProvider}
{autofocus}
fullHeight={false}
fixedInput={false}
>

View File

@ -31,6 +31,7 @@
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let showHeader: boolean = true
export let syncLocation = true
export let autofocus = true
const client = getClient()
const hierarchy = client.getHierarchy()
@ -143,7 +144,7 @@
{#if message}
{#key _id}
<ThreadContent bind:selectedMessageId {message} />
<ThreadContent bind:selectedMessageId {message} {autofocus} />
{/key}
{:else if isLoading}
<Loading />

View File

@ -269,6 +269,8 @@ export async function openChannelInSidebar (
const tab: ChatWidgetTab = {
id: `chunter_${_id}`,
objectId: object._id,
objectClass: object._class,
name,
icon: getChannelClassIcon(object),
iconComponent: isChannel ? undefined : iconMixin?.component,
@ -304,6 +306,8 @@ export async function openThreadInSidebarChannel (
): Promise<void> {
const newTab: ChatWidgetTab = {
...tab,
objectId: message._id,
objectClass: message._class,
name: await translate(chunter.string.ThreadIn, { name: tab.data.channelName }),
data: { ...tab.data, thread: message._id }
}
@ -314,10 +318,12 @@ export async function closeThreadInSidebarChannel (widget: Widget, tab: ChatWidg
const thread = tab.allowedPath !== undefined ? tab.data.thread : undefined
const newTab: ChatWidgetTab = {
...tab,
objectId: tab.data._id,
objectClass: tab.data._class,
id: tab.id.startsWith('thread_') ? generateId() : tab.id,
name: tab.data.channelName,
allowedPath: undefined,
data: { ...tab.data, thread: undefined }
data: { ...tab.data, thread: undefined, props: undefined }
}
createWidgetTab(widget, newTab)
@ -330,7 +336,8 @@ export async function openThreadInSidebar (
_id: Ref<ActivityMessage>,
msg?: ActivityMessage,
doc?: Doc,
selectedMessageId?: Ref<ActivityMessage>
selectedMessageId?: Ref<ActivityMessage>,
props?: Record<string, any>
): Promise<void> {
const client = getClient()
@ -371,13 +378,16 @@ export async function openThreadInSidebar (
id: 'thread_' + _id,
name: tabName,
icon: chunter.icon.Thread,
objectId: message._id,
objectClass: message._class,
allowedPath,
data: {
_id: object?._id,
_class: object?._class,
thread: message._id,
selectedMessageId,
channelName: name
channelName: name,
props
}
}
createWidgetTab(widget, tab, true)
@ -461,6 +471,8 @@ export async function locationDataResolver (loc: Location): Promise<LocationData
const name = (await getChannelName(_id, _class, object)) ?? (await translate(titleIntl, {}))
return {
objectId: object._id,
objectClass: object._class,
name,
icon: chunter.icon.Chunter,
iconComponent: isChunterSpace ? iconMixin?.component : undefined,

View File

@ -420,7 +420,7 @@ export function recheckNotifications (context: DocNotifyContext): void {
const toReadData = Array.from(toRead)
toRead.clear()
void (async () => {
const _client = client.apply(undefined, 'recheckNotifications')
const _client = client.apply(undefined, 'recheckNotifications', true)
await inboxClient.readNotifications(_client, toReadData)
await _client.commit()
})()
@ -436,7 +436,7 @@ export async function readChannelMessages (
}
const inboxClient = InboxNotificationsClientImpl.getClient()
const client = getClient().apply(undefined, 'readViewportMessages')
const op = getClient().apply(undefined, 'readViewportMessages', true)
try {
const allIds = getAllIds(messages)
@ -469,11 +469,11 @@ export async function readChannelMessages (
store.set(context._id, newTimestamp)
return store
})
await client.update(context, { lastViewedTimestamp: newTimestamp })
await op.update(context, { lastViewedTimestamp: newTimestamp })
}
await inboxClient.readNotifications(client, [...notifications, ...relatedMentions])
await inboxClient.readNotifications(op, [...notifications, ...relatedMentions])
} finally {
await client.commit()
await op.commit()
}
}

View File

@ -107,6 +107,7 @@ export interface ChatWidgetTab extends WidgetTab {
thread?: Ref<ActivityMessage>
channelName: string
selectedMessageId?: Ref<ActivityMessage>
props?: Record<string, any>
}
}
@ -252,7 +253,13 @@ export default plugin(chunterId, {
function: {
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
OpenThreadInSidebar: '' as Resource<
(_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc, selectedId?: Ref<ActivityMessage>) => Promise<void>
(
_id: Ref<ActivityMessage>,
msg?: ActivityMessage,
doc?: Doc,
selectedId?: Ref<ActivityMessage>,
props?: Record<string, any>
) => Promise<void>
>,
OpenChannelInSidebar: '' as Resource<
(

View File

@ -73,7 +73,7 @@
const prev = lastId
lastId = _id
if (prev !== undefined) {
void inboxClient.then((client) => client.readDoc(getClient(), prev))
void inboxClient.then((client) => client.readDoc(prev))
}
query.query(contact.class.Organization, { _id }, (result) => {
object = result[0]
@ -82,7 +82,7 @@
}
onDestroy(async () => {
void inboxClient.then((client) => client.readDoc(getClient(), _id))
void inboxClient.then((client) => client.readDoc(_id))
})
</script>

View File

@ -105,13 +105,13 @@
if (lastId !== _id) {
const prev = lastId
lastId = _id
void notificationClient.then((client) => client.readDoc(getClient(), prev))
void notificationClient.then((client) => client.readDoc(prev))
}
}
onDestroy(async () => {
controlledDocumentClosed()
void notificationClient.then((client) => client.readDoc(getClient(), _id))
void notificationClient.then((client) => client.readDoc(_id))
})
$: if (_id && _class && project) {

View File

@ -92,12 +92,12 @@
if (lastId !== _id) {
const prev = lastId
lastId = _id
void notificationClient.then((client) => client.readDoc(getClient(), prev))
void notificationClient.then((client) => client.readDoc(prev))
}
}
onDestroy(async () => {
void notificationClient.then((client) => client.readDoc(getClient(), _id))
void notificationClient.then((client) => client.readDoc(_id))
})
const starredQuery = createQuery()

View File

@ -64,7 +64,7 @@
{ attachedTo: channelId },
(res) => {
plainMessages = res
inboxClient.readDoc(getClient(), channelId)
inboxClient.readDoc(channelId)
},
{ sort: { sendOn: SortingOrder.Descending } }
)
@ -93,7 +93,7 @@
)
}
)
await inboxClient.readDoc(getClient(), channel._id)
await inboxClient.readDoc(channel._id)
clear()
}

View File

@ -52,7 +52,7 @@
let integrations: Integration[] = []
let selectedIntegration: Integration | undefined = undefined
channel && inboxClient.forceReadDoc(getClient(), channel._id, channel._class)
channel && inboxClient.forceReadDoc(channel._id, channel._class)
const dispatch = createEventDispatcher()

View File

@ -82,7 +82,7 @@
objectId
)
Analytics.handleEvent(GmailEvents.SentEmail, { to: channel.value })
await inboxClient.forceReadDoc(getClient(), channel._id, channel._class)
await inboxClient.forceReadDoc(channel._id, channel._class)
objectId = generateId()
dispatch('close')
}

View File

@ -96,7 +96,7 @@
.filter((m) => m.length)
})
Analytics.handleEvent(GmailEvents.SentEmail, { to: channel.value })
await inboxClient.forceReadDoc(getClient(), channel._id, channel._class)
await inboxClient.forceReadDoc(channel._id, channel._class)
for (const attachment of attachments) {
await client.addCollection(
attachmentP.class.Attachment,

View File

@ -55,6 +55,7 @@
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/workbench": "^0.6.16",
"svelte": "^4.2.12"
}
}

View File

@ -13,14 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import { getCurrentAccount } from '@hcengineering/core'
import notification, { BrowserNotification, NotificationStatus } from '@hcengineering/notification'
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import notification, { BrowserNotification } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { addNotification, getCurrentResolvedLocation, Location, NotificationSeverity } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { parseLinkId } from '@hcengineering/view-resources'
import { Analytics } from '@hcengineering/analytics'
import workbench, { Application } from '@hcengineering/workbench'
import { getResource } from '@hcengineering/platform'
import { checkPermission, pushAllowed, subscribePush } from '../utils'
import { NotificationSeverity, addNotification } from '@hcengineering/ui'
import Notification from './Notification.svelte'
async function check (allowed: boolean) {
async function check (allowed: boolean): Promise<void> {
if (allowed) {
query.unsubscribe()
return
@ -38,26 +44,69 @@
query.query(
notification.class.BrowserNotification,
{
user: getCurrentAccount()._id,
status: NotificationStatus.New,
createdOn: { $gt: Date.now() }
user: getCurrentAccount()._id
},
(res) => {
if (res.length > 0) {
notify(res[0])
void notify(res[0])
}
}
)
}
const client = getClient()
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
async function getObjectIdFromLocation (loc: Location): Promise<string | undefined> {
const appAlias = loc.path[2]
const application = client.getModel().findAllSync<Application>(workbench.class.Application, { alias: appAlias })[0]
if (application?.locationDataResolver != null) {
const resolver = await getResource(application.locationDataResolver)
const data = await resolver(loc)
return data.objectId
} else {
if (loc.fragment == null) return
const [, id, _class] = decodeURIComponent(loc.fragment).split('|')
if (_class == null) return
try {
return await parseLinkId(linkProviders, id, _class as Ref<Class<Doc>>)
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
}
}
}
async function notify (value: BrowserNotification): Promise<void> {
addNotification(value.title, value.body, Notification, { value }, NotificationSeverity.Info)
await client.update(value, { status: NotificationStatus.Notified })
const _id: Ref<Doc> | undefined = value.objectId
const getSidebarObject = await getResource(workbench.function.GetSidebarObject)
const sidebarObjectId = getSidebarObject()?._id
if (_id && _id === sidebarObjectId) {
await client.remove(value)
return
}
const locObjectId = await getObjectIdFromLocation(getCurrentResolvedLocation())
if (_id && _id === locObjectId) {
await client.remove(value)
return
}
addNotification(
value.title,
value.body,
Notification,
{ value },
NotificationSeverity.Info,
`notification-${value.objectId}`
)
await client.remove(value)
}
const query = createQuery()
$: check($pushAllowed)
$: void check($pushAllowed)
</script>

View File

@ -30,16 +30,12 @@
import InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte'
import NotifyContextIcon from './NotifyContextIcon.svelte'
import {
archiveContextNotifications,
isActivityNotification,
isMentionNotification,
unarchiveContextNotifications
} from '../utils'
import { isActivityNotification, isMentionNotification } from '../utils'
export let value: DocNotifyContext
export let notifications: WithLookup<DisplayInboxNotification>[]
export let viewlets: ActivityNotificationViewlet[] = []
export let isArchiving = false
export let archived = false
const maxNotifications = 3
@ -174,13 +170,8 @@
isActionMenuOpened = false
}
let archivingPromise: Promise<any> | undefined = undefined
async function checkContext (): Promise<void> {
await archivingPromise
archivingPromise = archived ? unarchiveContextNotifications(value) : archiveContextNotifications(value)
await archivingPromise
archivingPromise = undefined
dispatch('archive')
}
// function canShowTooltip (group: InboxNotification[]): boolean {
@ -231,7 +222,7 @@
<div class="actions clear-mins">
<div class="flex-center min-w-6">
{#if archivingPromise !== undefined}
{#if isArchiving}
<Spinner size="small" />
{:else}
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />
@ -284,7 +275,7 @@
<div class="actions clear-mins">
<div class="flex-center">
{#if archivingPromise !== undefined}
{#if isArchiving}
<Spinner size="small" />
{:else}
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />

View File

@ -15,14 +15,16 @@
<script lang="ts">
import activity, { ActivityMessage } from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import { Class, Doc, getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
import { Class, Doc, getCurrentAccount, groupByArray, Ref, SortingOrder } from '@hcengineering/core'
import { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import {
AnyComponent,
closePanel,
Component,
defineSeparators,
deviceOptionsStore as deviceInfo,
getCurrentLocation,
Label,
Location,
location as locationStore,
@ -30,9 +32,7 @@
Scroller,
Separator,
TabItem,
TabList,
closePanel,
getCurrentLocation
TabList
} from '@hcengineering/ui'
import view, { decodeObjectURI } from '@hcengineering/view'
import { parseLinkId } from '@hcengineering/view-resources'
@ -142,7 +142,7 @@
}
}
$: filteredData = filterData(filter, selectedTabId, inboxData, $contextByIdStore)
$: filteredData = filterData(filter, selectedTabId, inboxData)
const unsubscribeLoc = locationStore.subscribe((newLocation) => {
void syncLocation(newLocation)
@ -192,7 +192,7 @@
if (thread !== undefined) {
const fn = await getResource(chunter.function.OpenThreadInSidebar)
void fn(thread, undefined, undefined, selectedMessageId)
void fn(thread, undefined, undefined, selectedMessageId, { autofocus: false })
}
if (selectedMessageId !== undefined) {
@ -308,23 +308,10 @@
}
}
function filterNotifications (
filter: InboxNotificationsFilter,
notifications: InboxNotification[]
): InboxNotification[] {
switch (filter) {
case 'unread':
return notifications.filter(({ isViewed }) => !isViewed)
case 'all':
return notifications
}
}
function filterData (
filter: InboxNotificationsFilter,
selectedTabId: string | number,
inboxData: InboxData,
contextById: IdMap<DocNotifyContext>
inboxData: InboxData
): InboxData {
if (selectedTabId === allTab.id && filter === 'all') {
return inboxData
@ -333,18 +320,20 @@
const result = new Map()
for (const [key, notifications] of inboxData) {
const resNotifications = filterNotifications(filter, notifications)
if (filter === 'unread' && key !== selectedContext?._id && !notifications.some(({ isViewed }) => !isViewed)) {
continue
}
if (resNotifications.length === 0) {
if (notifications.length === 0) {
continue
}
if (selectedTabId === allTab.id) {
result.set(key, resNotifications)
result.set(key, notifications)
continue
}
const context = contextById.get(key)
const context = $contextByIdStore.get(key)
if (context === undefined) {
continue
@ -354,9 +343,9 @@
selectedTabId === activity.class.ActivityMessage &&
hierarchy.isDerived(context.objectClass, activity.class.ActivityMessage)
) {
result.set(key, resNotifications)
result.set(key, notifications)
} else if (context.objectClass === selectedTabId) {
result.set(key, resNotifications)
result.set(key, notifications)
}
}
@ -372,11 +361,13 @@
function onArchiveToggled (): void {
showArchive = !showArchive
selectedTabId = allTab.id
void selectContext(undefined)
}
function onUnreadsToggled (): void {
filter = filter === 'unread' ? 'all' : 'unread'
localStorage.setItem('inbox-filter', filter)
void selectContext(undefined)
}
$: items = [
@ -459,9 +450,10 @@
_class: isChunterChannel(selectedContext, urlObjectClass)
? urlObjectClass ?? selectedContext.objectClass
: selectedContext.objectClass,
autofocus: false,
context: selectedContext,
activityMessage: selectedMessage,
props: { context: selectedContext }
props: { context: selectedContext, autofocus: false }
}}
on:close={() => selectContext(undefined)}
/>

View File

@ -18,7 +18,7 @@
DisplayInboxNotification,
DocNotifyContext
} from '@hcengineering/notification'
import { Ref } from '@hcengineering/core'
import { Ref, Timestamp } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte'
import { ListView } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
@ -40,6 +40,10 @@
let list: ListView
let listSelection = 0
let element: HTMLDivElement | undefined
let prevArchived = false
let archivingContexts = new Set<Ref<DocNotifyContext>>()
let archivedContexts = new Map<Ref<DocNotifyContext>, Timestamp>()
let displayData: [Ref<DocNotifyContext>, DisplayInboxNotification[]][] = []
let viewlets: ActivityNotificationViewlet[] = []
@ -48,15 +52,61 @@
viewlets = res
})
$: if (prevArchived !== archived) {
prevArchived = archived
archivedContexts.clear()
}
$: updateDisplayData(data)
function updateDisplayData (data: InboxData): void {
displayData = Array.from(data.entries()).sort(([, notifications1], [, notifications2]) =>
let result: [Ref<DocNotifyContext>, DisplayInboxNotification[]][] = Array.from(data.entries())
if (archivedContexts.size > 0) {
result = result.filter(([contextId]) => {
const context = $contextByIdStore.get(contextId)
return (
!archivedContexts.has(contextId) ||
(context?.lastUpdateTimestamp ?? 0) > (archivedContexts.get(contextId) ?? 0)
)
})
}
displayData = result.sort(([, notifications1], [, notifications2]) =>
notificationsComparator(notifications1[0], notifications2[0])
)
}
function onKeydown (key: KeyboardEvent): void {
async function archiveContext (listSelection: number): Promise<void> {
const contextId = displayData[listSelection]?.[0]
const context = $contextByIdStore.get(contextId)
if (contextId === undefined || context === undefined) {
return
}
archivingContexts = archivingContexts.add(contextId)
try {
const nextContextId = displayData[listSelection + 1]?.[0] ?? displayData[listSelection - 1]?.[0]
const nextContext = $contextByIdStore.get(nextContextId)
if (archived) {
void unarchiveContextNotifications(context)
} else {
void archiveContextNotifications(context)
}
list.select(Math.min(listSelection, displayData.length - 2))
archivedContexts = archivedContexts.set(contextId, context.lastUpdateTimestamp ?? 0)
displayData = displayData.filter(([id]) => id !== contextId)
if (selectedContext === contextId || selectedContext === undefined) {
dispatch('click', { context: nextContext })
}
} catch (e) {}
archivingContexts.delete(contextId)
archivingContexts = archivingContexts
}
async function onKeydown (key: KeyboardEvent): Promise<void> {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
@ -71,14 +121,7 @@
key.preventDefault()
key.stopPropagation()
const contextId = displayData[listSelection]?.[0]
const context = $contextByIdStore.get(contextId)
if (archived) {
void unarchiveContextNotifications(context)
} else {
void archiveContextNotifications(context)
}
await archiveContext(listSelection)
}
if (key.code === 'Enter') {
key.preventDefault()
@ -126,6 +169,8 @@
notifications={contextNotifications}
{archived}
{viewlets}
isArchiving={archivingContexts.has(contextId)}
on:archive={() => archiveContext(itemIndex)}
on:click={(event) => {
dispatch('click', event.detail)
listSelection = itemIndex

View File

@ -148,13 +148,15 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
return InboxNotificationsClientImpl._instance
}
async readDoc (client: TxOperations, _id: Ref<Doc>): Promise<void> {
async readDoc (_id: Ref<Doc>): Promise<void> {
const docNotifyContext = this._contextByDoc.get(_id)
if (docNotifyContext === undefined) {
return
}
const client = getClient()
const op = client.apply(undefined, 'readDoc', true)
const inboxNotifications = await client.findAll(
notification.class.InboxNotification,
{ docNotifyContext: docNotifyContext._id, isViewed: false },
@ -162,19 +164,20 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
)
for (const notification of inboxNotifications) {
await client.updateDoc(notification._class, notification.space, notification._id, { isViewed: true })
await op.updateDoc(notification._class, notification.space, notification._id, { isViewed: true })
}
await client.update(docNotifyContext, { lastViewedTimestamp: Date.now() })
await op.update(docNotifyContext, { lastViewedTimestamp: Date.now() })
}
async forceReadDoc (client: TxOperations, _id: Ref<Doc>, _class: Ref<Class<Doc>>): Promise<void> {
async forceReadDoc (_id: Ref<Doc>, _class: Ref<Class<Doc>>): Promise<void> {
const context = this._contextByDoc.get(_id)
if (context !== undefined) {
await this.readDoc(client, _id)
await this.readDoc(_id)
return
}
const client = getClient()
const doc = await client.findOne(_class, { _id })
if (doc === undefined) {
@ -230,7 +233,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
async archiveNotifications (client: TxOperations, ids: Array<Ref<InboxNotification>>): Promise<void> {
const inboxNotifications = (get(this.inboxNotifications) ?? []).filter(({ _id }) => ids.includes(_id))
for (const notification of inboxNotifications) {
await client.update(notification, { archived: true })
await client.update(notification, { archived: true, isViewed: true })
}
}
@ -248,7 +251,10 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
)
const contexts = get(this.contexts) ?? []
for (const notification of inboxNotifications) {
await ops.updateDoc(notification._class, notification.space, notification._id, { archived: true })
await ops.updateDoc(notification._class, notification.space, notification._id, {
archived: true,
isViewed: true
})
}
for (const context of contexts) {

View File

@ -44,7 +44,8 @@ import {
unreadAll,
checkPermission,
unarchiveContextNotifications,
isNotificationAllowed
isNotificationAllowed,
locationDataResolver
} from './utils'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
@ -77,7 +78,8 @@ export default async (): Promise<Resources> => ({
CanUnReadNotifyContext: canUnReadNotifyContext,
HasInboxNotifications: hasInboxNotifications,
CheckPushPermission: checkPermission,
IsNotificationAllowed: isNotificationAllowed
IsNotificationAllowed: isNotificationAllowed,
LocationDataResolver: locationDataResolver
},
actionImpl: {
Unsubscribe: unsubscribe,

View File

@ -38,7 +38,6 @@ import core, {
type WithLookup
} from '@hcengineering/core'
import notification, {
NotificationStatus,
notificationId,
type ActivityInboxNotification,
type BaseNotificationType,
@ -63,9 +62,10 @@ import {
type Location,
type ResolvedLocation
} from '@hcengineering/ui'
import { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
import { getObjectLinkId } from '@hcengineering/view-resources'
import view, { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
import { getObjectLinkId, parseLinkId } from '@hcengineering/view-resources'
import { get, writable } from 'svelte/store'
import type { LocationData } from '@hcengineering/workbench'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
import { type InboxData, type InboxNotificationsFilter } from './types'
@ -183,7 +183,7 @@ export async function archiveContextNotifications (doc?: DocNotifyContext): Prom
return
}
const ops = getClient().apply(undefined, 'archiveContextNotifications')
const ops = getClient().apply(undefined, 'archiveContextNotifications', true)
try {
const notifications = await ops.findAll(
@ -209,7 +209,7 @@ export async function unarchiveContextNotifications (doc?: DocNotifyContext): Pr
return
}
const ops = getClient().apply(undefined, 'unarchiveContextNotifications')
const ops = getClient().apply(undefined, 'unarchiveContextNotifications', true)
try {
const notifications = await ops.findAll(
@ -790,11 +790,10 @@ export async function subscribePush (): Promise<boolean> {
async function cleanTag (_id: Ref<Doc>): Promise<void> {
const client = getClient()
const notifications = await client.findAll(notification.class.BrowserNotification, {
tag: _id,
status: NotificationStatus.New
tag: _id
})
for (const notification of notifications) {
await client.update(notification, { status: NotificationStatus.Notified })
await client.remove(notification)
}
}
@ -858,3 +857,20 @@ export function isNotificationAllowed (type: BaseNotificationType, providerId: R
return type.defaultEnabled
}
export async function locationDataResolver (loc: Location): Promise<LocationData> {
const client = getClient()
try {
const [id, _class] = decodeObjectURI(loc.path[3])
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
const _id: Ref<Doc> | undefined = await parseLinkId(linkProviders, id, _class)
return {
objectId: _id,
objectClass: _class
}
} catch (e) {
return {}
}
}

View File

@ -52,7 +52,6 @@ export const DOMAIN_USER_NOTIFY = 'notification-user' as Domain
*/
export interface BrowserNotification extends Doc {
user: Ref<Account>
status: NotificationStatus
title: string
body: string
onClickLocation?: Location
@ -84,14 +83,6 @@ export interface PushSubscription extends Doc {
keys: PushSubscriptionKeys
}
/**
* @public
*/
export enum NotificationStatus {
New,
Notified
}
/**
* @public
*/
@ -314,8 +305,8 @@ export interface InboxNotificationsClient {
activityInboxNotifications: Writable<ActivityInboxNotification[]>
inboxNotificationsByContext: Readable<Map<Ref<DocNotifyContext>, InboxNotification[]>>
readDoc: (client: TxOperations, _id: Ref<Doc>) => Promise<void>
forceReadDoc: (client: TxOperations, _id: Ref<Doc>, _class: Ref<Class<Doc>>) => Promise<void>
readDoc: (_id: Ref<Doc>) => Promise<void>
forceReadDoc: (_id: Ref<Doc>, _class: Ref<Class<Doc>>) => Promise<void>
readNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>
unreadNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>
archiveNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>

View File

@ -55,12 +55,12 @@
if (lastId !== _id) {
const prev = lastId
lastId = _id
void notificationClient.then((client) => client.readDoc(getClient(), prev))
void notificationClient.then((client) => client.readDoc(prev))
}
}
onDestroy(async () => {
void notificationClient.then((client) => client.readDoc(getClient(), _id))
void notificationClient.then((client) => client.readDoc(_id))
})
const query = createQuery()

View File

@ -68,12 +68,12 @@
if (lastId !== _id) {
const prev = lastId
lastId = _id
void notificationClient.then((client) => client.readDoc(getClient(), prev))
void notificationClient.then((client) => client.readDoc(prev))
}
}
onDestroy(async () => {
void notificationClient.then((client) => client.readDoc(getClient(), _id))
void notificationClient.then((client) => client.readDoc(_id))
})
const query = createQuery()

View File

@ -44,7 +44,7 @@
const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
onDestroy(async () => {
void inboxClient.then((client) => client.readDoc(getClient(), _id))
void inboxClient.then((client) => client.readDoc(_id))
})
const client = getClient()
@ -57,7 +57,7 @@
const prev = lastId
lastId = _id
if (prev !== undefined) {
void inboxClient.then((client) => client.readDoc(getClient(), prev))
void inboxClient.then((client) => client.readDoc(prev))
}
query.query(recruit.class.Vacancy, { _id }, (result) => {
object = result[0] as Required<Vacancy>

View File

@ -89,7 +89,7 @@
(res) => {
messages = res.reverse()
if (channel !== undefined) {
inboxClient.forceReadDoc(client, channel._id, channel._class)
inboxClient.forceReadDoc(channel._id, channel._class)
}
},
{
@ -150,7 +150,7 @@
}
)
if (channel !== undefined) {
await inboxClient.forceReadDoc(client, channel._id, channel._class)
await inboxClient.forceReadDoc(channel._id, channel._class)
}
clear()
}

View File

@ -89,13 +89,13 @@
if (_id && lastId && lastId !== _id) {
const prev = lastId
lastId = _id
void inboxClient.readDoc(getClient(), prev)
void inboxClient.readDoc(prev)
}
}
onDestroy(async () => {
if (issueId === undefined) return
void inboxClient.readDoc(getClient(), issueId)
void inboxClient.readDoc(issueId)
})
$: if (issueId !== undefined && _class !== undefined) {

View File

@ -54,12 +54,12 @@
if (lastId !== _id) {
const prev = lastId
lastId = _id
void inboxClient.then((client) => client.readDoc(getClient(), prev))
void inboxClient.then((client) => client.readDoc(prev))
}
}
onDestroy(async () => {
void inboxClient.then((client) => client.readDoc(getClient(), _id))
void inboxClient.then((client) => client.readDoc(_id))
})
$: _id !== undefined &&

View File

@ -65,7 +65,7 @@
const prev = lastId
lastId = _id
void inboxClient.then(async (client) => {
await client.readDoc(pClient, prev)
await client.readDoc(prev)
})
}
}
@ -73,7 +73,7 @@
onDestroy(async () => {
await inboxClient.then(async (client) => {
if (objectId === undefined) return
await client.readDoc(pClient, objectId)
await client.readDoc(objectId)
})
})

View File

@ -215,9 +215,11 @@
} else {
const tabToReplace = tabs.findLast((t) => !t.isPinned)
if (tabToReplace !== undefined) {
await client.update(tabToReplace, {
const op = client.apply(undefined, undefined, true)
await op.update(tabToReplace, {
location: url
})
await op.commit()
selectTab(tabToReplace._id)
prevTabIdStore.set(tabToReplace._id)
} else {

View File

@ -26,7 +26,7 @@ import ServerManager from './components/ServerManager.svelte'
import WorkbenchTabs from './components/WorkbenchTabs.svelte'
import { isAdminUser } from '@hcengineering/presentation'
import { canCloseTab, closeTab, pinTab, unpinTab } from './workbench'
import { closeWidgetTab, createWidgetTab } from './sidebar'
import { closeWidgetTab, createWidgetTab, getSidebarObject } from './sidebar'
async function hasArchiveSpaces (spaces: Space[]): Promise<boolean> {
return spaces.find((sp) => sp.archived) !== undefined
@ -57,7 +57,8 @@ export default async (): Promise<Resources> => ({
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser(),
CanCloseTab: canCloseTab,
CreateWidgetTab: createWidgetTab,
CloseWidgetTab: closeWidgetTab
CloseWidgetTab: closeWidgetTab,
GetSidebarObject: getSidebarObject
},
actionImpl: {
Navigate: doNavigate,

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import { WorkbenchEvents, type Widget, type WidgetTab } from '@hcengineering/workbench'
import { getCurrentAccount, type Ref } from '@hcengineering/core'
import { type Class, type Doc, getCurrentAccount, type Ref } from '@hcengineering/core'
import { get, writable } from 'svelte/store'
import { getCurrentLocation } from '@hcengineering/ui'
import { getResource } from '@hcengineering/platform'
@ -31,6 +31,8 @@ export interface WidgetState {
data?: Record<string, any>
tabs: WidgetTab[]
tab?: string
objectId?: Ref<Doc>
objectClass?: Ref<Class<Doc>>
closedByUser?: boolean
openedByUser?: boolean
}
@ -347,3 +349,22 @@ export function updateTabData (widget: Ref<Widget>, tabId: string, data: Record<
widgetsState
})
}
export function getSidebarObject (): Partial<Pick<Doc, '_id' | '_class'>> {
const state = get(sidebarStore)
if (state.variant !== SidebarVariant.EXPANDED || state.widget == null) {
return {}
}
const { widgetsState } = state
const widgetState = widgetsState.get(state.widget)
if (widgetState == null) {
return {}
}
const tab = widgetState.tabs.find((it) => it.id === widgetState.tab)
return {
_id: tab?.objectId ?? widgetState.objectId,
_class: tab?.objectClass ?? widgetState.objectClass
}
}

View File

@ -121,7 +121,9 @@ const syncTabLoc = reduceCalls(async (): Promise<void> => {
// return
// }
await getClient().diffUpdate(tab, { location: url, name })
const op = getClient().apply(undefined, undefined, true)
await op.diffUpdate(tab, { location: url, name })
await op.commit()
}
})

View File

@ -32,6 +32,8 @@ import { ViewAction } from '@hcengineering/view'
*/
export interface LocationData {
objectId?: Ref<Doc>
objectClass?: Ref<Class<Doc>>
name?: string
nameIntl?: IntlString
icon?: Asset
@ -93,6 +95,8 @@ export interface WidgetTab {
widget?: Ref<Widget>
isPinned?: boolean
allowedPath?: string
objectId?: Ref<Doc>
objectClass?: Ref<Class<Doc>>
data?: Record<string, any>
}
@ -258,7 +262,8 @@ export default plugin(workbenchId, {
},
function: {
CreateWidgetTab: '' as Resource<(widget: Widget, tab: WidgetTab, newTab: boolean) => Promise<void>>,
CloseWidgetTab: '' as Resource<(widget: Widget, tab: string) => Promise<void>>
CloseWidgetTab: '' as Resource<(widget: Widget, tab: string) => Promise<void>>,
GetSidebarObject: '' as Resource<() => Partial<Pick<Doc, '_id' | '_class'>>>
},
actionImpl: {
Navigate: '' as ViewAction<{

View File

@ -63,7 +63,6 @@ import notification, {
InboxNotification,
MentionInboxNotification,
notificationId,
NotificationStatus,
NotificationType,
PushData,
PushSubscription
@ -545,7 +544,6 @@ export async function createPushFromInbox (
)
return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiver.space, {
user: receiver._id,
status: NotificationStatus.New,
title,
body,
senderId: sender._id,

View File

@ -210,7 +210,6 @@ test.describe('Channel tests', () => {
await channelPageSecond.sendMessage('One two')
await channelPageSecond.checkMessageExist('One two', true, 'One two')
await channelPage.clickChannel('random')
await channelPage.clickOnClosePopupButton()
await channelPage.clickChannel('general')
await channelPage.checkMessageExist('One two', true, 'One two')
})
@ -236,7 +235,6 @@ test.describe('Channel tests', () => {
await channelPageSecond.sendMessage('One two')
await channelPageSecond.checkMessageExist('One two', true, 'One two')
await channelPage.clickChannel('general')
await channelPage.clickOnClosePopupButton()
await channelPage.clickChannel('random')
await channelPage.checkMessageExist('One two', true, 'One two')
})

View File

@ -111,7 +111,7 @@ test.describe('candidate/talents tests', () => {
mergeLocation: true,
location: firstLocation,
mergeTitle: true,
title: titleTalent1,
title: titleTalent2,
mergeSource: true,
source: sourceTalent1
})
@ -121,7 +121,7 @@ test.describe('candidate/talents tests', () => {
await talentsPage.openTalentByTalentName(talentNameFirst)
await talentDetailsPage.checkSocialLinks('Phone', '123123213213')
await talentDetailsPage.checkSocialLinks('Email', 'test-merge-2@gmail.com')
await talentDetailsPage.checkMergeContacts(firstLocation, titleTalent2, sourceTalent2)
await talentDetailsPage.checkMergeContacts(firstLocation, titleTalent2, sourceTalent1)
})
test('Match to vacancy', async () => {