mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-22 07:26:19 +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 Space,
|
||||||
type Timestamp,
|
type Timestamp,
|
||||||
type Tx,
|
type Tx,
|
||||||
type TxCUD
|
type TxCUD,
|
||||||
|
DOMAIN_TRANSIENT
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import {
|
import {
|
||||||
ArrOf,
|
ArrOf,
|
||||||
@ -75,7 +76,6 @@ import {
|
|||||||
type NotificationProvider,
|
type NotificationProvider,
|
||||||
type NotificationProviderDefaults,
|
type NotificationProviderDefaults,
|
||||||
type NotificationProviderSetting,
|
type NotificationProviderSetting,
|
||||||
type NotificationStatus,
|
|
||||||
type NotificationTemplate,
|
type NotificationTemplate,
|
||||||
type NotificationType,
|
type NotificationType,
|
||||||
type NotificationTypeSetting,
|
type NotificationTypeSetting,
|
||||||
@ -92,7 +92,7 @@ export { notificationId, DOMAIN_USER_NOTIFY, DOMAIN_NOTIFICATION, DOMAIN_DOC_NOT
|
|||||||
export { notificationOperation } from './migration'
|
export { notificationOperation } from './migration'
|
||||||
export { notification as default }
|
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 {
|
export class TBrowserNotification extends TDoc implements BrowserNotification {
|
||||||
senderId?: Ref<Account> | undefined
|
senderId?: Ref<Account> | undefined
|
||||||
tag!: Ref<Doc<Space>>
|
tag!: Ref<Doc<Space>>
|
||||||
@ -100,7 +100,6 @@ export class TBrowserNotification extends TDoc implements BrowserNotification {
|
|||||||
body!: string
|
body!: string
|
||||||
onClickLocation?: Location | undefined
|
onClickLocation?: Location | undefined
|
||||||
user!: Ref<Account>
|
user!: Ref<Account>
|
||||||
status!: NotificationStatus
|
|
||||||
messageId?: Ref<ActivityMessage>
|
messageId?: Ref<ActivityMessage>
|
||||||
messageClass?: Ref<Class<ActivityMessage>>
|
messageClass?: Ref<Class<ActivityMessage>>
|
||||||
objectId!: Ref<Doc>
|
objectId!: Ref<Doc>
|
||||||
@ -368,6 +367,10 @@ export function createModel (builder: Builder): void {
|
|||||||
TNotificationProviderDefaults
|
TNotificationProviderDefaults
|
||||||
)
|
)
|
||||||
|
|
||||||
|
builder.mixin(notification.class.BrowserNotification, core.class.Class, core.mixin.TransientConfiguration, {
|
||||||
|
broadcastOnly: true
|
||||||
|
})
|
||||||
|
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
setting.class.SettingsCategory,
|
setting.class.SettingsCategory,
|
||||||
core.space.Model,
|
core.space.Model,
|
||||||
@ -389,6 +392,7 @@ export function createModel (builder: Builder): void {
|
|||||||
{
|
{
|
||||||
label: notification.string.Inbox,
|
label: notification.string.Inbox,
|
||||||
icon: notification.icon.Notifications,
|
icon: notification.icon.Notifications,
|
||||||
|
locationDataResolver: notification.function.LocationDataResolver,
|
||||||
alias: notificationId,
|
alias: notificationId,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
locationResolver: notification.resolver.Location,
|
locationResolver: notification.resolver.Location,
|
||||||
|
@ -23,7 +23,6 @@ import {
|
|||||||
} from '@hcengineering/model'
|
} from '@hcengineering/model'
|
||||||
import notification, {
|
import notification, {
|
||||||
notificationId,
|
notificationId,
|
||||||
NotificationStatus,
|
|
||||||
type BrowserNotification,
|
type BrowserNotification,
|
||||||
type DocNotifyContext,
|
type DocNotifyContext,
|
||||||
type InboxNotification
|
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) => {
|
func: async (client) => {
|
||||||
await client.deleteMany(DOMAIN_TX, {
|
await client.deleteMany(DOMAIN_TX, {
|
||||||
_class: core.class.TxUpdateDoc,
|
_class: core.class.TxUpdateDoc,
|
||||||
@ -410,14 +409,28 @@ export const notificationOperation: MigrateOperation = {
|
|||||||
$exists: true
|
$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> {}
|
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 { type Doc, type Ref } from '@hcengineering/core'
|
||||||
import notification, { notificationId } from '@hcengineering/notification'
|
import notification, { notificationId } from '@hcengineering/notification'
|
||||||
import { type IntlString, type Resource, mergeIds } from '@hcengineering/platform'
|
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 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'
|
import { type DocUpdateMessageViewlet } from '@hcengineering/activity'
|
||||||
|
|
||||||
export default mergeIds(notificationId, notification, {
|
export default mergeIds(notificationId, notification, {
|
||||||
@ -53,7 +53,8 @@ export default mergeIds(notificationId, notification, {
|
|||||||
HasDocNotifyContextPinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
HasDocNotifyContextPinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
HasDocNotifyContextUnpinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
HasDocNotifyContextUnpinAction: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
CanReadNotifyContext: '' 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: {
|
category: {
|
||||||
Notification: '' as Ref<ActionCategory>
|
Notification: '' as Ref<ActionCategory>
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
} from '@hcengineering/model'
|
} from '@hcengineering/model'
|
||||||
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
|
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
|
||||||
import workbench from '@hcengineering/workbench'
|
import workbench from '@hcengineering/workbench'
|
||||||
|
import core, { DOMAIN_TX } from '@hcengineering/core'
|
||||||
|
|
||||||
import { workbenchId } from '.'
|
import { workbenchId } from '.'
|
||||||
|
|
||||||
@ -33,6 +34,15 @@ export const workbenchOperation: MigrateOperation = {
|
|||||||
{
|
{
|
||||||
state: 'remove-wrong-tabs-v1',
|
state: 'remove-wrong-tabs-v1',
|
||||||
func: removeTabs
|
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'
|
message: 'child'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
let attempt = 0
|
|
||||||
const pp = new Promise((resolve) => {
|
const pp = new Promise((resolve) => {
|
||||||
liveQuery.query<AttachedComment>(
|
liveQuery.query<AttachedComment>(
|
||||||
test.class.TestComment,
|
test.class.TestComment,
|
||||||
@ -483,14 +482,10 @@ describe('query', () => {
|
|||||||
(result) => {
|
(result) => {
|
||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
if (comment !== undefined) {
|
||||||
if (attempt++ > 0) {
|
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space?._id).toEqual(
|
||||||
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space?._id).toEqual(
|
futureSpace._id
|
||||||
futureSpace._id
|
)
|
||||||
)
|
resolve(null)
|
||||||
resolve(null)
|
|
||||||
} else {
|
|
||||||
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } }
|
{ lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } }
|
||||||
@ -521,7 +516,6 @@ describe('query', () => {
|
|||||||
message: 'test'
|
message: 'test'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
let attempt = 0
|
|
||||||
const childLength = 3
|
const childLength = 3
|
||||||
const pp = new Promise((resolve) => {
|
const pp = new Promise((resolve) => {
|
||||||
liveQuery.query<AttachedComment>(
|
liveQuery.query<AttachedComment>(
|
||||||
@ -529,10 +523,13 @@ describe('query', () => {
|
|||||||
{ _id: parentComment },
|
{ _id: parentComment },
|
||||||
(result) => {
|
(result) => {
|
||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
const res = (comment.$lookup as any)?.comments?.length
|
||||||
expect((comment.$lookup as any)?.comments).toHaveLength(attempt++)
|
|
||||||
|
if (res !== undefined) {
|
||||||
|
expect(res).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(res).toBeLessThanOrEqual(childLength)
|
||||||
}
|
}
|
||||||
if (attempt === childLength) {
|
if ((res ?? 0) === childLength) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -628,23 +625,15 @@ describe('query', () => {
|
|||||||
message: 'child'
|
message: 'child'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
let attempt = -1
|
|
||||||
const pp = new Promise((resolve) => {
|
const pp = new Promise((resolve) => {
|
||||||
liveQuery.query<AttachedComment>(
|
liveQuery.query<AttachedComment>(
|
||||||
test.class.TestComment,
|
test.class.TestComment,
|
||||||
{ _id: childComment },
|
{ _id: childComment },
|
||||||
(result) => {
|
(result) => {
|
||||||
attempt++
|
|
||||||
const comment = result[0]
|
const comment = result[0]
|
||||||
if (comment !== undefined) {
|
if (comment !== undefined) {
|
||||||
if (attempt > 0) {
|
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
|
||||||
expect((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
|
resolve(null)
|
||||||
resolve(null)
|
|
||||||
} else {
|
|
||||||
expect(
|
|
||||||
((comment.$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space as Doc)?._id
|
|
||||||
).toEqual(futureSpace)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } }
|
{ lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } }
|
||||||
|
@ -95,6 +95,8 @@ export class LiveQuery implements WithTx, Client {
|
|||||||
private queryCounter: number = 0
|
private queryCounter: number = 0
|
||||||
private closed: boolean = false
|
private closed: boolean = false
|
||||||
|
|
||||||
|
private readonly queriesToUpdate = new Map<number, [Query, Doc[]]>()
|
||||||
|
|
||||||
// A map of _class to documents.
|
// A map of _class to documents.
|
||||||
private readonly documentRefs = new Map<string, Map<Ref<Doc>, DocumentRef>>()
|
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) {
|
if (q.options?.sort !== undefined) {
|
||||||
await resultSort(q.result, q.options?.sort, q._class, this.getHierarchy(), this.client.getModel())
|
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> {
|
private async refresh (q: Query): Promise<void> {
|
||||||
|
this.queriesToUpdate.delete(q.id)
|
||||||
await q.refresh()
|
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.options?.limit !== undefined && q.result.length > q.options.limit) {
|
||||||
if (q.result.pop()?._id !== doc._id || q.options?.total === true) {
|
if (q.result.pop()?._id !== doc._id || q.options?.total === true) {
|
||||||
await this.callback(q)
|
await this.callback(q, true)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
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) {
|
if (q.result instanceof Promise) {
|
||||||
q.result = await q.result
|
q.result = await q.result
|
||||||
}
|
}
|
||||||
@ -1059,9 +1062,15 @@ export class LiveQuery implements WithTx, Client {
|
|||||||
this.updateDocuments(q, q.result)
|
this.updateDocuments(q, q.result)
|
||||||
|
|
||||||
const result = 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 {
|
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) {
|
if (q.options?.sort !== undefined) {
|
||||||
await resultSort(q.result, q.options?.sort, q._class, this.getHierarchy(), this.getModel())
|
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) {
|
if (q.options?.total === true) {
|
||||||
q.total--
|
q.total--
|
||||||
}
|
}
|
||||||
await this.callback(q)
|
await this.callback(q, true)
|
||||||
}
|
}
|
||||||
await this.handleDocRemoveLookup(q, tx)
|
await this.handleDocRemoveLookup(q, tx)
|
||||||
}
|
}
|
||||||
@ -1220,7 +1229,7 @@ export class LiveQuery implements WithTx, Client {
|
|||||||
if (q.options?.sort !== undefined) {
|
if (q.options?.sort !== undefined) {
|
||||||
await resultSort(q.result, q.options?.sort, q._class, this.getHierarchy(), this.getModel())
|
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))
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1533,10 +1553,10 @@ export class LiveQuery implements WithTx, Client {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (q.result.pop()?._id !== updatedDoc._id) {
|
if (q.result.pop()?._id !== updatedDoc._id) {
|
||||||
await this.callback(q)
|
await this.callback(q, true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.callback(q)
|
await this.callback(q, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,11 +59,11 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.default.highlighted:hover {
|
&.default.highlighted:hover:not(.highlighted) {
|
||||||
background-color: var(--theme-popup-divider);
|
background-color: var(--theme-popup-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.lumia.highlighted:hover {
|
&.lumia.highlighted:hover:not(.highlighted) {
|
||||||
background-color: var(--global-ui-highlight-BackgroundColor);
|
background-color: var(--global-ui-highlight-BackgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { type AnyComponent, type AnySvelteComponent } from '../../types'
|
|||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
|
group?: string
|
||||||
component: AnyComponent | AnySvelteComponent
|
component: AnyComponent | AnySvelteComponent
|
||||||
subTitle?: string
|
subTitle?: string
|
||||||
subTitlePostfix?: string
|
subTitlePostfix?: string
|
||||||
|
@ -18,11 +18,19 @@ export const addNotification = (notification: Notification, store: Writable<Noti
|
|||||||
id: generateId()
|
id: generateId()
|
||||||
}
|
}
|
||||||
|
|
||||||
update((notifications: Notification[]) =>
|
update((notifications: Notification[]) => {
|
||||||
[NotificationPosition.TopRight, NotificationPosition.TopLeft].includes(newNotification.position)
|
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]
|
? [newNotification, ...notifications]
|
||||||
: [...notifications, newNotification]
|
: [...notifications, newNotification]
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeNotification = (notificationId: string, { update }: Writable<Notification[]>): void => {
|
export const removeNotification = (notificationId: string, { update }: Writable<Notification[]>): void => {
|
||||||
|
@ -103,7 +103,8 @@ export function addNotification (
|
|||||||
subTitle: string,
|
subTitle: string,
|
||||||
component: AnyComponent | AnySvelteComponent,
|
component: AnyComponent | AnySvelteComponent,
|
||||||
params?: Record<string, any>,
|
params?: Record<string, any>,
|
||||||
severity: NotificationSeverity = NotificationSeverity.Success
|
severity: NotificationSeverity = NotificationSeverity.Success,
|
||||||
|
group?: string
|
||||||
): void {
|
): void {
|
||||||
const closeTimeout = parseInt(localStorage.getItem('#platform.notification.timeout') ?? '10000')
|
const closeTimeout = parseInt(localStorage.getItem('#platform.notification.timeout') ?? '10000')
|
||||||
const notification: Notification = {
|
const notification: Notification = {
|
||||||
@ -111,6 +112,7 @@ export function addNotification (
|
|||||||
title,
|
title,
|
||||||
subTitle,
|
subTitle,
|
||||||
severity,
|
severity,
|
||||||
|
group,
|
||||||
position: NotificationPosition.BottomLeft,
|
position: NotificationPosition.BottomLeft,
|
||||||
component,
|
component,
|
||||||
closeTimeout,
|
closeTimeout,
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
export let filters: Ref<ActivityMessagesFilter>[] = []
|
export let filters: Ref<ActivityMessagesFilter>[] = []
|
||||||
export let isAsideOpened = false
|
export let isAsideOpened = false
|
||||||
export let syncLocation = true
|
export let syncLocation = true
|
||||||
|
export let autofocus = true
|
||||||
export let freeze = false
|
export let freeze = false
|
||||||
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
||||||
|
|
||||||
@ -108,6 +109,7 @@
|
|||||||
{collection}
|
{collection}
|
||||||
provider={dataProvider}
|
provider={dataProvider}
|
||||||
{freeze}
|
{freeze}
|
||||||
|
{autofocus}
|
||||||
loadMoreAllowed={!isDocChannel}
|
loadMoreAllowed={!isDocChannel}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
export let boundary: HTMLElement | undefined | null = undefined
|
export let boundary: HTMLElement | undefined | null = undefined
|
||||||
export let collection: string | undefined
|
export let collection: string | undefined
|
||||||
export let isThread = false
|
export let isThread = false
|
||||||
|
export let autofocus = true
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
@ -68,7 +69,7 @@
|
|||||||
<ActivityExtensionComponent
|
<ActivityExtensionComponent
|
||||||
kind="input"
|
kind="input"
|
||||||
{extensions}
|
{extensions}
|
||||||
props={{ object, boundary, collection, autofocus: true, withTypingInfo: true }}
|
props={{ object, boundary, collection, autofocus, withTypingInfo: true }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
export let _id: Ref<ChunterSpace>
|
export let _id: Ref<ChunterSpace>
|
||||||
export let _class: Ref<Class<ChunterSpace>>
|
export let _class: Ref<Class<ChunterSpace>>
|
||||||
|
export let autofocus = true
|
||||||
|
|
||||||
const objectQuery = createQuery()
|
const objectQuery = createQuery()
|
||||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||||
@ -37,5 +38,5 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if object}
|
{#if object}
|
||||||
<ChannelView {object} {context} on:close />
|
<ChannelView {object} {context} {autofocus} on:close />
|
||||||
{/if}
|
{/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}
|
{#if threadId && visible}
|
||||||
<div class="thread" style:height style:width>
|
<div class="thread" style:height style:width>
|
||||||
<ThreadView
|
<ThreadView
|
||||||
|
{...tab.data.props}
|
||||||
_id={threadId}
|
_id={threadId}
|
||||||
{selectedMessageId}
|
{selectedMessageId}
|
||||||
syncLocation={false}
|
syncLocation={false}
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
|
|
||||||
export let object: Doc
|
export let object: Doc
|
||||||
export let context: DocNotifyContext | undefined
|
export let context: DocNotifyContext | undefined
|
||||||
|
export let autofocus = true
|
||||||
export let embedded: boolean = false
|
export let embedded: boolean = false
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
@ -143,6 +144,7 @@
|
|||||||
{context}
|
{context}
|
||||||
{object}
|
{object}
|
||||||
{filters}
|
{filters}
|
||||||
|
{autofocus}
|
||||||
isAsideOpened={(withAside && isAsideShown) || isThreadOpened}
|
isAsideOpened={(withAside && isAsideShown) || isThreadOpened}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
export let fullHeight = true
|
export let fullHeight = true
|
||||||
export let freeze = false
|
export let freeze = false
|
||||||
export let loadMoreAllowed = true
|
export let loadMoreAllowed = true
|
||||||
|
export let autofocus = true
|
||||||
|
|
||||||
const minMsgHeightRem = 2
|
const minMsgHeightRem = 2
|
||||||
const loadMoreThreshold = 200
|
const loadMoreThreshold = 200
|
||||||
@ -400,9 +401,7 @@
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
const op = client.apply(undefined, 'chunter.scrollDown')
|
await inboxClient.readDoc(doc._id)
|
||||||
await inboxClient.readDoc(op, doc._id)
|
|
||||||
await op.commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let forceRead = false
|
let forceRead = false
|
||||||
@ -419,9 +418,7 @@
|
|||||||
|
|
||||||
if (unViewed.length === 0) {
|
if (unViewed.length === 0) {
|
||||||
forceRead = true
|
forceRead = true
|
||||||
const op = client.apply(undefined, 'chunter.forceReadContext', true)
|
await inboxClient.readDoc(object._id)
|
||||||
await inboxClient.readDoc(op, object._id)
|
|
||||||
await op.commit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,7 +639,7 @@
|
|||||||
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if !fixedInput}
|
{#if !fixedInput}
|
||||||
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} />
|
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} {autofocus} />
|
||||||
{/if}
|
{/if}
|
||||||
</BaseChatScroller>
|
</BaseChatScroller>
|
||||||
{#if !isThread && isLatestMessageButtonVisible}
|
{#if !isThread && isLatestMessageButtonVisible}
|
||||||
@ -659,7 +656,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if fixedInput}
|
{#if fixedInput}
|
||||||
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} />
|
<ChannelInput {object} {readonly} boundary={scrollDiv} {collection} {isThread} {autofocus} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
||||||
export let message: ActivityMessage
|
export let message: ActivityMessage
|
||||||
|
export let autofocus = true
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
@ -65,6 +66,7 @@
|
|||||||
object={message}
|
object={message}
|
||||||
{channel}
|
{channel}
|
||||||
provider={dataProvider}
|
provider={dataProvider}
|
||||||
|
{autofocus}
|
||||||
fullHeight={false}
|
fullHeight={false}
|
||||||
fixedInput={false}
|
fixedInput={false}
|
||||||
>
|
>
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
||||||
export let showHeader: boolean = true
|
export let showHeader: boolean = true
|
||||||
export let syncLocation = true
|
export let syncLocation = true
|
||||||
|
export let autofocus = true
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
@ -143,7 +144,7 @@
|
|||||||
|
|
||||||
{#if message}
|
{#if message}
|
||||||
{#key _id}
|
{#key _id}
|
||||||
<ThreadContent bind:selectedMessageId {message} />
|
<ThreadContent bind:selectedMessageId {message} {autofocus} />
|
||||||
{/key}
|
{/key}
|
||||||
{:else if isLoading}
|
{:else if isLoading}
|
||||||
<Loading />
|
<Loading />
|
||||||
|
@ -269,6 +269,8 @@ export async function openChannelInSidebar (
|
|||||||
|
|
||||||
const tab: ChatWidgetTab = {
|
const tab: ChatWidgetTab = {
|
||||||
id: `chunter_${_id}`,
|
id: `chunter_${_id}`,
|
||||||
|
objectId: object._id,
|
||||||
|
objectClass: object._class,
|
||||||
name,
|
name,
|
||||||
icon: getChannelClassIcon(object),
|
icon: getChannelClassIcon(object),
|
||||||
iconComponent: isChannel ? undefined : iconMixin?.component,
|
iconComponent: isChannel ? undefined : iconMixin?.component,
|
||||||
@ -304,6 +306,8 @@ export async function openThreadInSidebarChannel (
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const newTab: ChatWidgetTab = {
|
const newTab: ChatWidgetTab = {
|
||||||
...tab,
|
...tab,
|
||||||
|
objectId: message._id,
|
||||||
|
objectClass: message._class,
|
||||||
name: await translate(chunter.string.ThreadIn, { name: tab.data.channelName }),
|
name: await translate(chunter.string.ThreadIn, { name: tab.data.channelName }),
|
||||||
data: { ...tab.data, thread: message._id }
|
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 thread = tab.allowedPath !== undefined ? tab.data.thread : undefined
|
||||||
const newTab: ChatWidgetTab = {
|
const newTab: ChatWidgetTab = {
|
||||||
...tab,
|
...tab,
|
||||||
|
objectId: tab.data._id,
|
||||||
|
objectClass: tab.data._class,
|
||||||
id: tab.id.startsWith('thread_') ? generateId() : tab.id,
|
id: tab.id.startsWith('thread_') ? generateId() : tab.id,
|
||||||
name: tab.data.channelName,
|
name: tab.data.channelName,
|
||||||
allowedPath: undefined,
|
allowedPath: undefined,
|
||||||
data: { ...tab.data, thread: undefined }
|
data: { ...tab.data, thread: undefined, props: undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
createWidgetTab(widget, newTab)
|
createWidgetTab(widget, newTab)
|
||||||
@ -330,7 +336,8 @@ export async function openThreadInSidebar (
|
|||||||
_id: Ref<ActivityMessage>,
|
_id: Ref<ActivityMessage>,
|
||||||
msg?: ActivityMessage,
|
msg?: ActivityMessage,
|
||||||
doc?: Doc,
|
doc?: Doc,
|
||||||
selectedMessageId?: Ref<ActivityMessage>
|
selectedMessageId?: Ref<ActivityMessage>,
|
||||||
|
props?: Record<string, any>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
@ -371,13 +378,16 @@ export async function openThreadInSidebar (
|
|||||||
id: 'thread_' + _id,
|
id: 'thread_' + _id,
|
||||||
name: tabName,
|
name: tabName,
|
||||||
icon: chunter.icon.Thread,
|
icon: chunter.icon.Thread,
|
||||||
|
objectId: message._id,
|
||||||
|
objectClass: message._class,
|
||||||
allowedPath,
|
allowedPath,
|
||||||
data: {
|
data: {
|
||||||
_id: object?._id,
|
_id: object?._id,
|
||||||
_class: object?._class,
|
_class: object?._class,
|
||||||
thread: message._id,
|
thread: message._id,
|
||||||
selectedMessageId,
|
selectedMessageId,
|
||||||
channelName: name
|
channelName: name,
|
||||||
|
props
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createWidgetTab(widget, tab, true)
|
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, {}))
|
const name = (await getChannelName(_id, _class, object)) ?? (await translate(titleIntl, {}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
objectId: object._id,
|
||||||
|
objectClass: object._class,
|
||||||
name,
|
name,
|
||||||
icon: chunter.icon.Chunter,
|
icon: chunter.icon.Chunter,
|
||||||
iconComponent: isChunterSpace ? iconMixin?.component : undefined,
|
iconComponent: isChunterSpace ? iconMixin?.component : undefined,
|
||||||
|
@ -420,7 +420,7 @@ export function recheckNotifications (context: DocNotifyContext): void {
|
|||||||
const toReadData = Array.from(toRead)
|
const toReadData = Array.from(toRead)
|
||||||
toRead.clear()
|
toRead.clear()
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const _client = client.apply(undefined, 'recheckNotifications')
|
const _client = client.apply(undefined, 'recheckNotifications', true)
|
||||||
await inboxClient.readNotifications(_client, toReadData)
|
await inboxClient.readNotifications(_client, toReadData)
|
||||||
await _client.commit()
|
await _client.commit()
|
||||||
})()
|
})()
|
||||||
@ -436,7 +436,7 @@ export async function readChannelMessages (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||||
const client = getClient().apply(undefined, 'readViewportMessages')
|
const op = getClient().apply(undefined, 'readViewportMessages', true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const allIds = getAllIds(messages)
|
const allIds = getAllIds(messages)
|
||||||
@ -469,11 +469,11 @@ export async function readChannelMessages (
|
|||||||
store.set(context._id, newTimestamp)
|
store.set(context._id, newTimestamp)
|
||||||
return store
|
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 {
|
} finally {
|
||||||
await client.commit()
|
await op.commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +107,7 @@ export interface ChatWidgetTab extends WidgetTab {
|
|||||||
thread?: Ref<ActivityMessage>
|
thread?: Ref<ActivityMessage>
|
||||||
channelName: string
|
channelName: string
|
||||||
selectedMessageId?: Ref<ActivityMessage>
|
selectedMessageId?: Ref<ActivityMessage>
|
||||||
|
props?: Record<string, any>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +253,13 @@ export default plugin(chunterId, {
|
|||||||
function: {
|
function: {
|
||||||
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
OpenThreadInSidebar: '' as Resource<
|
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<
|
OpenChannelInSidebar: '' as Resource<
|
||||||
(
|
(
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
if (prev !== undefined) {
|
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) => {
|
query.query(contact.class.Organization, { _id }, (result) => {
|
||||||
object = result[0]
|
object = result[0]
|
||||||
@ -82,7 +82,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
void inboxClient.then((client) => client.readDoc(getClient(), _id))
|
void inboxClient.then((client) => client.readDoc(_id))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -105,13 +105,13 @@
|
|||||||
if (lastId !== _id) {
|
if (lastId !== _id) {
|
||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
void notificationClient.then((client) => client.readDoc(getClient(), prev))
|
void notificationClient.then((client) => client.readDoc(prev))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
controlledDocumentClosed()
|
controlledDocumentClosed()
|
||||||
void notificationClient.then((client) => client.readDoc(getClient(), _id))
|
void notificationClient.then((client) => client.readDoc(_id))
|
||||||
})
|
})
|
||||||
|
|
||||||
$: if (_id && _class && project) {
|
$: if (_id && _class && project) {
|
||||||
|
@ -92,12 +92,12 @@
|
|||||||
if (lastId !== _id) {
|
if (lastId !== _id) {
|
||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
void notificationClient.then((client) => client.readDoc(getClient(), prev))
|
void notificationClient.then((client) => client.readDoc(prev))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
void notificationClient.then((client) => client.readDoc(getClient(), _id))
|
void notificationClient.then((client) => client.readDoc(_id))
|
||||||
})
|
})
|
||||||
|
|
||||||
const starredQuery = createQuery()
|
const starredQuery = createQuery()
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
{ attachedTo: channelId },
|
{ attachedTo: channelId },
|
||||||
(res) => {
|
(res) => {
|
||||||
plainMessages = res
|
plainMessages = res
|
||||||
inboxClient.readDoc(getClient(), channelId)
|
inboxClient.readDoc(channelId)
|
||||||
},
|
},
|
||||||
{ sort: { sendOn: SortingOrder.Descending } }
|
{ sort: { sendOn: SortingOrder.Descending } }
|
||||||
)
|
)
|
||||||
@ -93,7 +93,7 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
await inboxClient.readDoc(getClient(), channel._id)
|
await inboxClient.readDoc(channel._id)
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
let integrations: Integration[] = []
|
let integrations: Integration[] = []
|
||||||
let selectedIntegration: Integration | undefined = undefined
|
let selectedIntegration: Integration | undefined = undefined
|
||||||
|
|
||||||
channel && inboxClient.forceReadDoc(getClient(), channel._id, channel._class)
|
channel && inboxClient.forceReadDoc(channel._id, channel._class)
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@
|
|||||||
objectId
|
objectId
|
||||||
)
|
)
|
||||||
Analytics.handleEvent(GmailEvents.SentEmail, { to: channel.value })
|
Analytics.handleEvent(GmailEvents.SentEmail, { to: channel.value })
|
||||||
await inboxClient.forceReadDoc(getClient(), channel._id, channel._class)
|
await inboxClient.forceReadDoc(channel._id, channel._class)
|
||||||
objectId = generateId()
|
objectId = generateId()
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@
|
|||||||
.filter((m) => m.length)
|
.filter((m) => m.length)
|
||||||
})
|
})
|
||||||
Analytics.handleEvent(GmailEvents.SentEmail, { to: channel.value })
|
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) {
|
for (const attachment of attachments) {
|
||||||
await client.addCollection(
|
await client.addCollection(
|
||||||
attachmentP.class.Attachment,
|
attachmentP.class.Attachment,
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
"@hcengineering/ui": "^0.6.15",
|
"@hcengineering/ui": "^0.6.15",
|
||||||
"@hcengineering/view": "^0.6.13",
|
"@hcengineering/view": "^0.6.13",
|
||||||
"@hcengineering/view-resources": "^0.6.0",
|
"@hcengineering/view-resources": "^0.6.0",
|
||||||
|
"@hcengineering/workbench": "^0.6.16",
|
||||||
"svelte": "^4.2.12"
|
"svelte": "^4.2.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,14 +13,20 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getCurrentAccount } from '@hcengineering/core'
|
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||||
import notification, { BrowserNotification, NotificationStatus } from '@hcengineering/notification'
|
import notification, { BrowserNotification } from '@hcengineering/notification'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
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 { checkPermission, pushAllowed, subscribePush } from '../utils'
|
||||||
import { NotificationSeverity, addNotification } from '@hcengineering/ui'
|
|
||||||
import Notification from './Notification.svelte'
|
import Notification from './Notification.svelte'
|
||||||
|
|
||||||
async function check (allowed: boolean) {
|
async function check (allowed: boolean): Promise<void> {
|
||||||
if (allowed) {
|
if (allowed) {
|
||||||
query.unsubscribe()
|
query.unsubscribe()
|
||||||
return
|
return
|
||||||
@ -38,26 +44,69 @@
|
|||||||
query.query(
|
query.query(
|
||||||
notification.class.BrowserNotification,
|
notification.class.BrowserNotification,
|
||||||
{
|
{
|
||||||
user: getCurrentAccount()._id,
|
user: getCurrentAccount()._id
|
||||||
status: NotificationStatus.New,
|
|
||||||
createdOn: { $gt: Date.now() }
|
|
||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
if (res.length > 0) {
|
if (res.length > 0) {
|
||||||
notify(res[0])
|
void notify(res[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = getClient()
|
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> {
|
async function notify (value: BrowserNotification): Promise<void> {
|
||||||
addNotification(value.title, value.body, Notification, { value }, NotificationSeverity.Info)
|
const _id: Ref<Doc> | undefined = value.objectId
|
||||||
await client.update(value, { status: NotificationStatus.Notified })
|
|
||||||
|
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()
|
const query = createQuery()
|
||||||
|
|
||||||
$: check($pushAllowed)
|
$: void check($pushAllowed)
|
||||||
</script>
|
</script>
|
||||||
|
@ -30,16 +30,12 @@
|
|||||||
|
|
||||||
import InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte'
|
import InboxNotificationPresenter from './inbox/InboxNotificationPresenter.svelte'
|
||||||
import NotifyContextIcon from './NotifyContextIcon.svelte'
|
import NotifyContextIcon from './NotifyContextIcon.svelte'
|
||||||
import {
|
import { isActivityNotification, isMentionNotification } from '../utils'
|
||||||
archiveContextNotifications,
|
|
||||||
isActivityNotification,
|
|
||||||
isMentionNotification,
|
|
||||||
unarchiveContextNotifications
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
export let value: DocNotifyContext
|
export let value: DocNotifyContext
|
||||||
export let notifications: WithLookup<DisplayInboxNotification>[]
|
export let notifications: WithLookup<DisplayInboxNotification>[]
|
||||||
export let viewlets: ActivityNotificationViewlet[] = []
|
export let viewlets: ActivityNotificationViewlet[] = []
|
||||||
|
export let isArchiving = false
|
||||||
export let archived = false
|
export let archived = false
|
||||||
|
|
||||||
const maxNotifications = 3
|
const maxNotifications = 3
|
||||||
@ -174,13 +170,8 @@
|
|||||||
isActionMenuOpened = false
|
isActionMenuOpened = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let archivingPromise: Promise<any> | undefined = undefined
|
|
||||||
|
|
||||||
async function checkContext (): Promise<void> {
|
async function checkContext (): Promise<void> {
|
||||||
await archivingPromise
|
dispatch('archive')
|
||||||
archivingPromise = archived ? unarchiveContextNotifications(value) : archiveContextNotifications(value)
|
|
||||||
await archivingPromise
|
|
||||||
archivingPromise = undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// function canShowTooltip (group: InboxNotification[]): boolean {
|
// function canShowTooltip (group: InboxNotification[]): boolean {
|
||||||
@ -231,7 +222,7 @@
|
|||||||
|
|
||||||
<div class="actions clear-mins">
|
<div class="actions clear-mins">
|
||||||
<div class="flex-center min-w-6">
|
<div class="flex-center min-w-6">
|
||||||
{#if archivingPromise !== undefined}
|
{#if isArchiving}
|
||||||
<Spinner size="small" />
|
<Spinner size="small" />
|
||||||
{:else}
|
{:else}
|
||||||
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />
|
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />
|
||||||
@ -284,7 +275,7 @@
|
|||||||
|
|
||||||
<div class="actions clear-mins">
|
<div class="actions clear-mins">
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
{#if archivingPromise !== undefined}
|
{#if isArchiving}
|
||||||
<Spinner size="small" />
|
<Spinner size="small" />
|
||||||
{:else}
|
{:else}
|
||||||
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />
|
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />
|
||||||
|
@ -15,14 +15,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||||
import chunter from '@hcengineering/chunter'
|
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 { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
||||||
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import {
|
import {
|
||||||
AnyComponent,
|
AnyComponent,
|
||||||
|
closePanel,
|
||||||
Component,
|
Component,
|
||||||
defineSeparators,
|
defineSeparators,
|
||||||
deviceOptionsStore as deviceInfo,
|
deviceOptionsStore as deviceInfo,
|
||||||
|
getCurrentLocation,
|
||||||
Label,
|
Label,
|
||||||
Location,
|
Location,
|
||||||
location as locationStore,
|
location as locationStore,
|
||||||
@ -30,9 +32,7 @@
|
|||||||
Scroller,
|
Scroller,
|
||||||
Separator,
|
Separator,
|
||||||
TabItem,
|
TabItem,
|
||||||
TabList,
|
TabList
|
||||||
closePanel,
|
|
||||||
getCurrentLocation
|
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import view, { decodeObjectURI } from '@hcengineering/view'
|
import view, { decodeObjectURI } from '@hcengineering/view'
|
||||||
import { parseLinkId } from '@hcengineering/view-resources'
|
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) => {
|
const unsubscribeLoc = locationStore.subscribe((newLocation) => {
|
||||||
void syncLocation(newLocation)
|
void syncLocation(newLocation)
|
||||||
@ -192,7 +192,7 @@
|
|||||||
|
|
||||||
if (thread !== undefined) {
|
if (thread !== undefined) {
|
||||||
const fn = await getResource(chunter.function.OpenThreadInSidebar)
|
const fn = await getResource(chunter.function.OpenThreadInSidebar)
|
||||||
void fn(thread, undefined, undefined, selectedMessageId)
|
void fn(thread, undefined, undefined, selectedMessageId, { autofocus: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedMessageId !== undefined) {
|
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 (
|
function filterData (
|
||||||
filter: InboxNotificationsFilter,
|
filter: InboxNotificationsFilter,
|
||||||
selectedTabId: string | number,
|
selectedTabId: string | number,
|
||||||
inboxData: InboxData,
|
inboxData: InboxData
|
||||||
contextById: IdMap<DocNotifyContext>
|
|
||||||
): InboxData {
|
): InboxData {
|
||||||
if (selectedTabId === allTab.id && filter === 'all') {
|
if (selectedTabId === allTab.id && filter === 'all') {
|
||||||
return inboxData
|
return inboxData
|
||||||
@ -333,18 +320,20 @@
|
|||||||
const result = new Map()
|
const result = new Map()
|
||||||
|
|
||||||
for (const [key, notifications] of inboxData) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedTabId === allTab.id) {
|
if (selectedTabId === allTab.id) {
|
||||||
result.set(key, resNotifications)
|
result.set(key, notifications)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = contextById.get(key)
|
const context = $contextByIdStore.get(key)
|
||||||
|
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
continue
|
continue
|
||||||
@ -354,9 +343,9 @@
|
|||||||
selectedTabId === activity.class.ActivityMessage &&
|
selectedTabId === activity.class.ActivityMessage &&
|
||||||
hierarchy.isDerived(context.objectClass, activity.class.ActivityMessage)
|
hierarchy.isDerived(context.objectClass, activity.class.ActivityMessage)
|
||||||
) {
|
) {
|
||||||
result.set(key, resNotifications)
|
result.set(key, notifications)
|
||||||
} else if (context.objectClass === selectedTabId) {
|
} else if (context.objectClass === selectedTabId) {
|
||||||
result.set(key, resNotifications)
|
result.set(key, notifications)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,11 +361,13 @@
|
|||||||
function onArchiveToggled (): void {
|
function onArchiveToggled (): void {
|
||||||
showArchive = !showArchive
|
showArchive = !showArchive
|
||||||
selectedTabId = allTab.id
|
selectedTabId = allTab.id
|
||||||
|
void selectContext(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUnreadsToggled (): void {
|
function onUnreadsToggled (): void {
|
||||||
filter = filter === 'unread' ? 'all' : 'unread'
|
filter = filter === 'unread' ? 'all' : 'unread'
|
||||||
localStorage.setItem('inbox-filter', filter)
|
localStorage.setItem('inbox-filter', filter)
|
||||||
|
void selectContext(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: items = [
|
$: items = [
|
||||||
@ -459,9 +450,10 @@
|
|||||||
_class: isChunterChannel(selectedContext, urlObjectClass)
|
_class: isChunterChannel(selectedContext, urlObjectClass)
|
||||||
? urlObjectClass ?? selectedContext.objectClass
|
? urlObjectClass ?? selectedContext.objectClass
|
||||||
: selectedContext.objectClass,
|
: selectedContext.objectClass,
|
||||||
|
autofocus: false,
|
||||||
context: selectedContext,
|
context: selectedContext,
|
||||||
activityMessage: selectedMessage,
|
activityMessage: selectedMessage,
|
||||||
props: { context: selectedContext }
|
props: { context: selectedContext, autofocus: false }
|
||||||
}}
|
}}
|
||||||
on:close={() => selectContext(undefined)}
|
on:close={() => selectContext(undefined)}
|
||||||
/>
|
/>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
DisplayInboxNotification,
|
DisplayInboxNotification,
|
||||||
DocNotifyContext
|
DocNotifyContext
|
||||||
} from '@hcengineering/notification'
|
} from '@hcengineering/notification'
|
||||||
import { Ref } from '@hcengineering/core'
|
import { Ref, Timestamp } from '@hcengineering/core'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { ListView } from '@hcengineering/ui'
|
import { ListView } from '@hcengineering/ui'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
@ -40,6 +40,10 @@
|
|||||||
let list: ListView
|
let list: ListView
|
||||||
let listSelection = 0
|
let listSelection = 0
|
||||||
let element: HTMLDivElement | undefined
|
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 displayData: [Ref<DocNotifyContext>, DisplayInboxNotification[]][] = []
|
||||||
let viewlets: ActivityNotificationViewlet[] = []
|
let viewlets: ActivityNotificationViewlet[] = []
|
||||||
@ -48,15 +52,61 @@
|
|||||||
viewlets = res
|
viewlets = res
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$: if (prevArchived !== archived) {
|
||||||
|
prevArchived = archived
|
||||||
|
archivedContexts.clear()
|
||||||
|
}
|
||||||
$: updateDisplayData(data)
|
$: updateDisplayData(data)
|
||||||
|
|
||||||
function updateDisplayData (data: InboxData): void {
|
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])
|
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') {
|
if (key.code === 'ArrowUp') {
|
||||||
key.stopPropagation()
|
key.stopPropagation()
|
||||||
key.preventDefault()
|
key.preventDefault()
|
||||||
@ -71,14 +121,7 @@
|
|||||||
key.preventDefault()
|
key.preventDefault()
|
||||||
key.stopPropagation()
|
key.stopPropagation()
|
||||||
|
|
||||||
const contextId = displayData[listSelection]?.[0]
|
await archiveContext(listSelection)
|
||||||
const context = $contextByIdStore.get(contextId)
|
|
||||||
|
|
||||||
if (archived) {
|
|
||||||
void unarchiveContextNotifications(context)
|
|
||||||
} else {
|
|
||||||
void archiveContextNotifications(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (key.code === 'Enter') {
|
if (key.code === 'Enter') {
|
||||||
key.preventDefault()
|
key.preventDefault()
|
||||||
@ -126,6 +169,8 @@
|
|||||||
notifications={contextNotifications}
|
notifications={contextNotifications}
|
||||||
{archived}
|
{archived}
|
||||||
{viewlets}
|
{viewlets}
|
||||||
|
isArchiving={archivingContexts.has(contextId)}
|
||||||
|
on:archive={() => archiveContext(itemIndex)}
|
||||||
on:click={(event) => {
|
on:click={(event) => {
|
||||||
dispatch('click', event.detail)
|
dispatch('click', event.detail)
|
||||||
listSelection = itemIndex
|
listSelection = itemIndex
|
||||||
|
@ -148,13 +148,15 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
|||||||
return InboxNotificationsClientImpl._instance
|
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)
|
const docNotifyContext = this._contextByDoc.get(_id)
|
||||||
|
|
||||||
if (docNotifyContext === undefined) {
|
if (docNotifyContext === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const op = client.apply(undefined, 'readDoc', true)
|
||||||
const inboxNotifications = await client.findAll(
|
const inboxNotifications = await client.findAll(
|
||||||
notification.class.InboxNotification,
|
notification.class.InboxNotification,
|
||||||
{ docNotifyContext: docNotifyContext._id, isViewed: false },
|
{ docNotifyContext: docNotifyContext._id, isViewed: false },
|
||||||
@ -162,19 +164,20 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
for (const notification of inboxNotifications) {
|
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)
|
const context = this._contextByDoc.get(_id)
|
||||||
|
|
||||||
if (context !== undefined) {
|
if (context !== undefined) {
|
||||||
await this.readDoc(client, _id)
|
await this.readDoc(_id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
const doc = await client.findOne(_class, { _id })
|
const doc = await client.findOne(_class, { _id })
|
||||||
|
|
||||||
if (doc === undefined) {
|
if (doc === undefined) {
|
||||||
@ -230,7 +233,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
|||||||
async archiveNotifications (client: TxOperations, ids: Array<Ref<InboxNotification>>): Promise<void> {
|
async archiveNotifications (client: TxOperations, ids: Array<Ref<InboxNotification>>): Promise<void> {
|
||||||
const inboxNotifications = (get(this.inboxNotifications) ?? []).filter(({ _id }) => ids.includes(_id))
|
const inboxNotifications = (get(this.inboxNotifications) ?? []).filter(({ _id }) => ids.includes(_id))
|
||||||
for (const notification of inboxNotifications) {
|
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) ?? []
|
const contexts = get(this.contexts) ?? []
|
||||||
for (const notification of inboxNotifications) {
|
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) {
|
for (const context of contexts) {
|
||||||
|
@ -44,7 +44,8 @@ import {
|
|||||||
unreadAll,
|
unreadAll,
|
||||||
checkPermission,
|
checkPermission,
|
||||||
unarchiveContextNotifications,
|
unarchiveContextNotifications,
|
||||||
isNotificationAllowed
|
isNotificationAllowed,
|
||||||
|
locationDataResolver
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
|
||||||
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
|
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
|
||||||
@ -77,7 +78,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
CanUnReadNotifyContext: canUnReadNotifyContext,
|
CanUnReadNotifyContext: canUnReadNotifyContext,
|
||||||
HasInboxNotifications: hasInboxNotifications,
|
HasInboxNotifications: hasInboxNotifications,
|
||||||
CheckPushPermission: checkPermission,
|
CheckPushPermission: checkPermission,
|
||||||
IsNotificationAllowed: isNotificationAllowed
|
IsNotificationAllowed: isNotificationAllowed,
|
||||||
|
LocationDataResolver: locationDataResolver
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
Unsubscribe: unsubscribe,
|
Unsubscribe: unsubscribe,
|
||||||
|
@ -38,7 +38,6 @@ import core, {
|
|||||||
type WithLookup
|
type WithLookup
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import notification, {
|
import notification, {
|
||||||
NotificationStatus,
|
|
||||||
notificationId,
|
notificationId,
|
||||||
type ActivityInboxNotification,
|
type ActivityInboxNotification,
|
||||||
type BaseNotificationType,
|
type BaseNotificationType,
|
||||||
@ -63,9 +62,10 @@ import {
|
|||||||
type Location,
|
type Location,
|
||||||
type ResolvedLocation
|
type ResolvedLocation
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
|
import view, { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
|
||||||
import { getObjectLinkId } from '@hcengineering/view-resources'
|
import { getObjectLinkId, parseLinkId } from '@hcengineering/view-resources'
|
||||||
import { get, writable } from 'svelte/store'
|
import { get, writable } from 'svelte/store'
|
||||||
|
import type { LocationData } from '@hcengineering/workbench'
|
||||||
|
|
||||||
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
|
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
|
||||||
import { type InboxData, type InboxNotificationsFilter } from './types'
|
import { type InboxData, type InboxNotificationsFilter } from './types'
|
||||||
@ -183,7 +183,7 @@ export async function archiveContextNotifications (doc?: DocNotifyContext): Prom
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const ops = getClient().apply(undefined, 'archiveContextNotifications')
|
const ops = getClient().apply(undefined, 'archiveContextNotifications', true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const notifications = await ops.findAll(
|
const notifications = await ops.findAll(
|
||||||
@ -209,7 +209,7 @@ export async function unarchiveContextNotifications (doc?: DocNotifyContext): Pr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const ops = getClient().apply(undefined, 'unarchiveContextNotifications')
|
const ops = getClient().apply(undefined, 'unarchiveContextNotifications', true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const notifications = await ops.findAll(
|
const notifications = await ops.findAll(
|
||||||
@ -790,11 +790,10 @@ export async function subscribePush (): Promise<boolean> {
|
|||||||
async function cleanTag (_id: Ref<Doc>): Promise<void> {
|
async function cleanTag (_id: Ref<Doc>): Promise<void> {
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const notifications = await client.findAll(notification.class.BrowserNotification, {
|
const notifications = await client.findAll(notification.class.BrowserNotification, {
|
||||||
tag: _id,
|
tag: _id
|
||||||
status: NotificationStatus.New
|
|
||||||
})
|
})
|
||||||
for (const notification of notifications) {
|
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
|
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 {
|
export interface BrowserNotification extends Doc {
|
||||||
user: Ref<Account>
|
user: Ref<Account>
|
||||||
status: NotificationStatus
|
|
||||||
title: string
|
title: string
|
||||||
body: string
|
body: string
|
||||||
onClickLocation?: Location
|
onClickLocation?: Location
|
||||||
@ -84,14 +83,6 @@ export interface PushSubscription extends Doc {
|
|||||||
keys: PushSubscriptionKeys
|
keys: PushSubscriptionKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export enum NotificationStatus {
|
|
||||||
New,
|
|
||||||
Notified
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -314,8 +305,8 @@ export interface InboxNotificationsClient {
|
|||||||
activityInboxNotifications: Writable<ActivityInboxNotification[]>
|
activityInboxNotifications: Writable<ActivityInboxNotification[]>
|
||||||
inboxNotificationsByContext: Readable<Map<Ref<DocNotifyContext>, InboxNotification[]>>
|
inboxNotificationsByContext: Readable<Map<Ref<DocNotifyContext>, InboxNotification[]>>
|
||||||
|
|
||||||
readDoc: (client: TxOperations, _id: Ref<Doc>) => Promise<void>
|
readDoc: (_id: Ref<Doc>) => Promise<void>
|
||||||
forceReadDoc: (client: TxOperations, _id: Ref<Doc>, _class: Ref<Class<Doc>>) => Promise<void>
|
forceReadDoc: (_id: Ref<Doc>, _class: Ref<Class<Doc>>) => Promise<void>
|
||||||
readNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>
|
readNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>
|
||||||
unreadNotifications: (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>
|
archiveNotifications: (client: TxOperations, ids: Array<Ref<InboxNotification>>) => Promise<void>
|
||||||
|
@ -55,12 +55,12 @@
|
|||||||
if (lastId !== _id) {
|
if (lastId !== _id) {
|
||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
void notificationClient.then((client) => client.readDoc(getClient(), prev))
|
void notificationClient.then((client) => client.readDoc(prev))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
void notificationClient.then((client) => client.readDoc(getClient(), _id))
|
void notificationClient.then((client) => client.readDoc(_id))
|
||||||
})
|
})
|
||||||
|
|
||||||
const query = createQuery()
|
const query = createQuery()
|
||||||
|
@ -68,12 +68,12 @@
|
|||||||
if (lastId !== _id) {
|
if (lastId !== _id) {
|
||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
void notificationClient.then((client) => client.readDoc(getClient(), prev))
|
void notificationClient.then((client) => client.readDoc(prev))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
void notificationClient.then((client) => client.readDoc(getClient(), _id))
|
void notificationClient.then((client) => client.readDoc(_id))
|
||||||
})
|
})
|
||||||
|
|
||||||
const query = createQuery()
|
const query = createQuery()
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
|
const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
void inboxClient.then((client) => client.readDoc(getClient(), _id))
|
void inboxClient.then((client) => client.readDoc(_id))
|
||||||
})
|
})
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
@ -57,7 +57,7 @@
|
|||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
if (prev !== undefined) {
|
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) => {
|
query.query(recruit.class.Vacancy, { _id }, (result) => {
|
||||||
object = result[0] as Required<Vacancy>
|
object = result[0] as Required<Vacancy>
|
||||||
|
@ -89,7 +89,7 @@
|
|||||||
(res) => {
|
(res) => {
|
||||||
messages = res.reverse()
|
messages = res.reverse()
|
||||||
if (channel !== undefined) {
|
if (channel !== undefined) {
|
||||||
inboxClient.forceReadDoc(client, channel._id, channel._class)
|
inboxClient.forceReadDoc(channel._id, channel._class)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -150,7 +150,7 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (channel !== undefined) {
|
if (channel !== undefined) {
|
||||||
await inboxClient.forceReadDoc(client, channel._id, channel._class)
|
await inboxClient.forceReadDoc(channel._id, channel._class)
|
||||||
}
|
}
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
|
@ -89,13 +89,13 @@
|
|||||||
if (_id && lastId && lastId !== _id) {
|
if (_id && lastId && lastId !== _id) {
|
||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
void inboxClient.readDoc(getClient(), prev)
|
void inboxClient.readDoc(prev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
if (issueId === undefined) return
|
if (issueId === undefined) return
|
||||||
void inboxClient.readDoc(getClient(), issueId)
|
void inboxClient.readDoc(issueId)
|
||||||
})
|
})
|
||||||
|
|
||||||
$: if (issueId !== undefined && _class !== undefined) {
|
$: if (issueId !== undefined && _class !== undefined) {
|
||||||
|
@ -54,12 +54,12 @@
|
|||||||
if (lastId !== _id) {
|
if (lastId !== _id) {
|
||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
void inboxClient.then((client) => client.readDoc(getClient(), prev))
|
void inboxClient.then((client) => client.readDoc(prev))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
void inboxClient.then((client) => client.readDoc(getClient(), _id))
|
void inboxClient.then((client) => client.readDoc(_id))
|
||||||
})
|
})
|
||||||
|
|
||||||
$: _id !== undefined &&
|
$: _id !== undefined &&
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
const prev = lastId
|
const prev = lastId
|
||||||
lastId = _id
|
lastId = _id
|
||||||
void inboxClient.then(async (client) => {
|
void inboxClient.then(async (client) => {
|
||||||
await client.readDoc(pClient, prev)
|
await client.readDoc(prev)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +73,7 @@
|
|||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
await inboxClient.then(async (client) => {
|
await inboxClient.then(async (client) => {
|
||||||
if (objectId === undefined) return
|
if (objectId === undefined) return
|
||||||
await client.readDoc(pClient, objectId)
|
await client.readDoc(objectId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -215,9 +215,11 @@
|
|||||||
} else {
|
} else {
|
||||||
const tabToReplace = tabs.findLast((t) => !t.isPinned)
|
const tabToReplace = tabs.findLast((t) => !t.isPinned)
|
||||||
if (tabToReplace !== undefined) {
|
if (tabToReplace !== undefined) {
|
||||||
await client.update(tabToReplace, {
|
const op = client.apply(undefined, undefined, true)
|
||||||
|
await op.update(tabToReplace, {
|
||||||
location: url
|
location: url
|
||||||
})
|
})
|
||||||
|
await op.commit()
|
||||||
selectTab(tabToReplace._id)
|
selectTab(tabToReplace._id)
|
||||||
prevTabIdStore.set(tabToReplace._id)
|
prevTabIdStore.set(tabToReplace._id)
|
||||||
} else {
|
} else {
|
||||||
|
@ -26,7 +26,7 @@ import ServerManager from './components/ServerManager.svelte'
|
|||||||
import WorkbenchTabs from './components/WorkbenchTabs.svelte'
|
import WorkbenchTabs from './components/WorkbenchTabs.svelte'
|
||||||
import { isAdminUser } from '@hcengineering/presentation'
|
import { isAdminUser } from '@hcengineering/presentation'
|
||||||
import { canCloseTab, closeTab, pinTab, unpinTab } from './workbench'
|
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> {
|
async function hasArchiveSpaces (spaces: Space[]): Promise<boolean> {
|
||||||
return spaces.find((sp) => sp.archived) !== undefined
|
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(),
|
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser(),
|
||||||
CanCloseTab: canCloseTab,
|
CanCloseTab: canCloseTab,
|
||||||
CreateWidgetTab: createWidgetTab,
|
CreateWidgetTab: createWidgetTab,
|
||||||
CloseWidgetTab: closeWidgetTab
|
CloseWidgetTab: closeWidgetTab,
|
||||||
|
GetSidebarObject: getSidebarObject
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
Navigate: doNavigate,
|
Navigate: doNavigate,
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
import { WorkbenchEvents, type Widget, type WidgetTab } from '@hcengineering/workbench'
|
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 { get, writable } from 'svelte/store'
|
||||||
import { getCurrentLocation } from '@hcengineering/ui'
|
import { getCurrentLocation } from '@hcengineering/ui'
|
||||||
import { getResource } from '@hcengineering/platform'
|
import { getResource } from '@hcengineering/platform'
|
||||||
@ -31,6 +31,8 @@ export interface WidgetState {
|
|||||||
data?: Record<string, any>
|
data?: Record<string, any>
|
||||||
tabs: WidgetTab[]
|
tabs: WidgetTab[]
|
||||||
tab?: string
|
tab?: string
|
||||||
|
objectId?: Ref<Doc>
|
||||||
|
objectClass?: Ref<Class<Doc>>
|
||||||
closedByUser?: boolean
|
closedByUser?: boolean
|
||||||
openedByUser?: boolean
|
openedByUser?: boolean
|
||||||
}
|
}
|
||||||
@ -347,3 +349,22 @@ export function updateTabData (widget: Ref<Widget>, tabId: string, data: Record<
|
|||||||
widgetsState
|
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
|
// 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 {
|
export interface LocationData {
|
||||||
|
objectId?: Ref<Doc>
|
||||||
|
objectClass?: Ref<Class<Doc>>
|
||||||
name?: string
|
name?: string
|
||||||
nameIntl?: IntlString
|
nameIntl?: IntlString
|
||||||
icon?: Asset
|
icon?: Asset
|
||||||
@ -93,6 +95,8 @@ export interface WidgetTab {
|
|||||||
widget?: Ref<Widget>
|
widget?: Ref<Widget>
|
||||||
isPinned?: boolean
|
isPinned?: boolean
|
||||||
allowedPath?: string
|
allowedPath?: string
|
||||||
|
objectId?: Ref<Doc>
|
||||||
|
objectClass?: Ref<Class<Doc>>
|
||||||
data?: Record<string, any>
|
data?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +262,8 @@ export default plugin(workbenchId, {
|
|||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
CreateWidgetTab: '' as Resource<(widget: Widget, tab: WidgetTab, newTab: boolean) => Promise<void>>,
|
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: {
|
actionImpl: {
|
||||||
Navigate: '' as ViewAction<{
|
Navigate: '' as ViewAction<{
|
||||||
|
@ -63,7 +63,6 @@ import notification, {
|
|||||||
InboxNotification,
|
InboxNotification,
|
||||||
MentionInboxNotification,
|
MentionInboxNotification,
|
||||||
notificationId,
|
notificationId,
|
||||||
NotificationStatus,
|
|
||||||
NotificationType,
|
NotificationType,
|
||||||
PushData,
|
PushData,
|
||||||
PushSubscription
|
PushSubscription
|
||||||
@ -545,7 +544,6 @@ export async function createPushFromInbox (
|
|||||||
)
|
)
|
||||||
return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiver.space, {
|
return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiver.space, {
|
||||||
user: receiver._id,
|
user: receiver._id,
|
||||||
status: NotificationStatus.New,
|
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
senderId: sender._id,
|
senderId: sender._id,
|
||||||
|
@ -210,7 +210,6 @@ test.describe('Channel tests', () => {
|
|||||||
await channelPageSecond.sendMessage('One two')
|
await channelPageSecond.sendMessage('One two')
|
||||||
await channelPageSecond.checkMessageExist('One two', true, 'One two')
|
await channelPageSecond.checkMessageExist('One two', true, 'One two')
|
||||||
await channelPage.clickChannel('random')
|
await channelPage.clickChannel('random')
|
||||||
await channelPage.clickOnClosePopupButton()
|
|
||||||
await channelPage.clickChannel('general')
|
await channelPage.clickChannel('general')
|
||||||
await channelPage.checkMessageExist('One two', true, 'One two')
|
await channelPage.checkMessageExist('One two', true, 'One two')
|
||||||
})
|
})
|
||||||
@ -236,7 +235,6 @@ test.describe('Channel tests', () => {
|
|||||||
await channelPageSecond.sendMessage('One two')
|
await channelPageSecond.sendMessage('One two')
|
||||||
await channelPageSecond.checkMessageExist('One two', true, 'One two')
|
await channelPageSecond.checkMessageExist('One two', true, 'One two')
|
||||||
await channelPage.clickChannel('general')
|
await channelPage.clickChannel('general')
|
||||||
await channelPage.clickOnClosePopupButton()
|
|
||||||
await channelPage.clickChannel('random')
|
await channelPage.clickChannel('random')
|
||||||
await channelPage.checkMessageExist('One two', true, 'One two')
|
await channelPage.checkMessageExist('One two', true, 'One two')
|
||||||
})
|
})
|
||||||
|
@ -111,7 +111,7 @@ test.describe('candidate/talents tests', () => {
|
|||||||
mergeLocation: true,
|
mergeLocation: true,
|
||||||
location: firstLocation,
|
location: firstLocation,
|
||||||
mergeTitle: true,
|
mergeTitle: true,
|
||||||
title: titleTalent1,
|
title: titleTalent2,
|
||||||
mergeSource: true,
|
mergeSource: true,
|
||||||
source: sourceTalent1
|
source: sourceTalent1
|
||||||
})
|
})
|
||||||
@ -121,7 +121,7 @@ test.describe('candidate/talents tests', () => {
|
|||||||
await talentsPage.openTalentByTalentName(talentNameFirst)
|
await talentsPage.openTalentByTalentName(talentNameFirst)
|
||||||
await talentDetailsPage.checkSocialLinks('Phone', '123123213213')
|
await talentDetailsPage.checkSocialLinks('Phone', '123123213213')
|
||||||
await talentDetailsPage.checkSocialLinks('Email', 'test-merge-2@gmail.com')
|
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 () => {
|
test('Match to vacancy', async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user