mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-21 15:02:13 +00:00
UBERF-8547: Inbox cleanup and other (#7058)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
400772b792
commit
f258c6755a
@ -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,
|
||||
|
@ -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> {}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -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 }] } }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 => {
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
@ -97,6 +97,7 @@
|
||||
{#if threadId && visible}
|
||||
<div class="thread" style:height style:width>
|
||||
<ThreadView
|
||||
{...tab.data.props}
|
||||
_id={threadId}
|
||||
{selectedMessageId}
|
||||
syncLocation={false}
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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 />
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<
|
||||
(
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
|
@ -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)}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 &&
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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<{
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
})
|
||||
|
@ -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 () => {
|
||||
|
Loading…
Reference in New Issue
Block a user