From e15e398e0cd4abc61221545fbce92db8502012db Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Fri, 19 Sep 2025 19:56:14 +0700 Subject: [PATCH] fix: use pulse for presence (#9896) Signed-off-by: Alexander Onnikov --- .../chat-message/ChatMessageInput.svelte | 4 +- .../components/message/MessageInput.svelte | 2 +- .../src/components/PresenceAvatars.svelte | 60 ++++++++++++--- .../src/components/PresenceContext.svelte | 37 ++++++++-- plugins/presence-resources/src/index.ts | 3 +- plugins/presence-resources/src/presence.ts | 69 +++++++++++++++++ plugins/presence-resources/src/pulse.ts | 41 +--------- plugins/presence-resources/src/store.ts | 51 +------------ plugins/presence-resources/src/typing.ts | 74 +++++++++++++++++++ 9 files changed, 233 insertions(+), 108 deletions(-) create mode 100644 plugins/presence-resources/src/presence.ts create mode 100644 plugins/presence-resources/src/typing.ts diff --git a/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte b/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte index 6245426a01..65cf266fb2 100644 --- a/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte +++ b/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte @@ -102,14 +102,14 @@ async function deleteTypingInfo (): Promise { if (!withTypingInfo) return - clearTyping(me, object._id) + void clearTyping(me, object._id) } async function updateTypingInfo (): Promise { if (!withTypingInfo) return throttle.call(() => { - setTyping(me, object._id) + void setTyping(me, object._id) }) } diff --git a/plugins/communication-resources/src/components/message/MessageInput.svelte b/plugins/communication-resources/src/components/message/MessageInput.svelte index 27a45f8149..05f3da949b 100644 --- a/plugins/communication-resources/src/components/message/MessageInput.svelte +++ b/plugins/communication-resources/src/components/message/MessageInput.svelte @@ -139,7 +139,7 @@ await editMessage(message, markdown, blobsToLoad, linksToLoad, appletsToLoad) } - clearTyping(me, card._id) + void clearTyping(me, card._id) } async function attachApplets (messageId: MessageID, appletDrafts: AppletDraft[]): Promise { diff --git a/plugins/presence-resources/src/components/PresenceAvatars.svelte b/plugins/presence-resources/src/components/PresenceAvatars.svelte index 56428276fd..5a8fa2e7a6 100644 --- a/plugins/presence-resources/src/components/PresenceAvatars.svelte +++ b/plugins/presence-resources/src/components/PresenceAvatars.svelte @@ -14,26 +14,66 @@ --> {#if persons.length > 0} diff --git a/plugins/presence-resources/src/components/PresenceContext.svelte b/plugins/presence-resources/src/components/PresenceContext.svelte index c288232756..14b2c44ed0 100644 --- a/plugins/presence-resources/src/components/PresenceContext.svelte +++ b/plugins/presence-resources/src/components/PresenceContext.svelte @@ -14,20 +14,43 @@ --> diff --git a/plugins/presence-resources/src/index.ts b/plugins/presence-resources/src/index.ts index becb54bdb3..dfa54428eb 100644 --- a/plugins/presence-resources/src/index.ts +++ b/plugins/presence-resources/src/index.ts @@ -21,9 +21,10 @@ import WorkbenchExtension from './components/WorkbenchExtension.svelte' import { getFollowee, publishData, followeeDataSubscribe, followeeDataUnsubscribe } from './store' export { Presence, PresenceAvatars } -export { updateMyPresence, removeMyPresence, presenceByObjectId } from './store' export * from './pulse' +export * from './presence' +export * from './typing' export * from './types' export default async (): Promise => ({ diff --git a/plugins/presence-resources/src/presence.ts b/plugins/presence-resources/src/presence.ts new file mode 100644 index 0000000000..a2a95df570 --- /dev/null +++ b/plugins/presence-resources/src/presence.ts @@ -0,0 +1,69 @@ +// Copyright © 2025 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 + +import { type Person } from '@hcengineering/contact' +import { type UnsubscribeCallback, type Callback } from '@hcengineering/hulypulse-client' +import { type Class, type Doc, type Ref } from '@hcengineering/core' +import { getMetadata } from '@hcengineering/platform' +import presentation from '@hcengineering/presentation' + +import { createPulseClient } from './pulse' + +export interface PresenceInfo { + personId: Ref + objectId: Ref + objectClass: Ref> +} + +export async function subscribePresence ( + objectClass: Ref>, + objectId: Ref, + callback: Callback +): Promise { + const client = await createPulseClient() + + if (client !== undefined) { + const workspace = getMetadata(presentation.metadata.WorkspaceUuid) ?? '' + return await client.subscribe(`${workspace}/presence/${objectId}/`, callback) + } + + return async () => false +} + +export async function updatePresence (presence: PresenceInfo, presenceTtlSeconds: number): Promise { + const client = await createPulseClient() + + if (client !== undefined) { + const workspace = getMetadata(presentation.metadata.WorkspaceUuid) ?? '' + const { personId, objectId } = presence + try { + await client.put(`${workspace}/presence/${objectId}/${personId}`, presence, presenceTtlSeconds) + } catch (error) { + console.warn('failed to put presence info:', error) + } + } +} + +export async function deletePresence (presence: PresenceInfo): Promise { + const client = await createPulseClient() + + if (client !== undefined) { + const workspace = getMetadata(presentation.metadata.WorkspaceUuid) ?? '' + const { personId, objectId } = presence + try { + await client.delete(`${workspace}/presence/${objectId}/${personId}`) + } catch (error) { + console.warn('failed to delete presence info:', error) + } + } +} diff --git a/plugins/presence-resources/src/pulse.ts b/plugins/presence-resources/src/pulse.ts index 07ab39afba..52b379090e 100644 --- a/plugins/presence-resources/src/pulse.ts +++ b/plugins/presence-resources/src/pulse.ts @@ -11,20 +11,12 @@ // See the License for the specific language governing permissions and // limitations under the License -import { type Person, getCurrentEmployee } from '@hcengineering/contact' -import { HulypulseClient, type UnsubscribeCallback, type Callback } from '@hcengineering/hulypulse-client' +import { HulypulseClient } from '@hcengineering/hulypulse-client' import { getMetadata } from '@hcengineering/platform' import presentation from '@hcengineering/presentation' -import { type Doc, type Ref } from '@hcengineering/core' - -const typingDelaySeconds = 2 let pulseclient: HulypulseClient | undefined -function getWorkspace (): string { - return getMetadata(presentation.metadata.WorkspaceUuid) ?? '' -} - export async function createPulseClient (): Promise { if (pulseclient == null) { const wsPulseUrl = getMetadata(presentation.metadata.PulseUrl) @@ -35,37 +27,6 @@ export async function createPulseClient (): Promise return pulseclient } -export interface TypingInfo { - personId: Ref - objectId: Ref -} - -export async function subscribeTyping ( - objectId: Ref, - callback: Callback -): Promise { - const workspace = getWorkspace() - return (await pulseclient?.subscribe(`${workspace}/typing/${objectId}/`, callback)) ?? (async () => false) -} - -export async function setTyping (me: string, objectId: Ref): Promise { - const workspace = getWorkspace() - const personId = getCurrentEmployee() - const typingInfo: TypingInfo = { personId, objectId } - try { - await pulseclient?.put(`${workspace}/typing/${objectId}/${me}`, typingInfo, typingDelaySeconds) - } catch (error) { - console.warn('failed to put typing info:', error) - } -} - -export function clearTyping (me: string, objectId: string): void { - const workspace = getWorkspace() - void pulseclient?.delete(`${workspace}/typing/${objectId}/${me}`).catch((error) => { - console.warn('failed to delete typing info:', error) - }) -} - export function closePulseClient (): void { pulseclient?.close() pulseclient = undefined diff --git a/plugins/presence-resources/src/store.ts b/plugins/presence-resources/src/store.ts index eee9750b77..2b7de1f309 100644 --- a/plugins/presence-resources/src/store.ts +++ b/plugins/presence-resources/src/store.ts @@ -13,13 +13,12 @@ // limitations under the License. // -import { type Doc, type Ref } from '@hcengineering/core' -import { getCurrentEmployee, type Person } from '@hcengineering/contact' -import { type PresenceData } from '@hcengineering/presence' +import { type Ref } from '@hcengineering/core' +import { type Person } from '@hcengineering/contact' import { getPersonByPersonRef } from '@hcengineering/contact-resources' -import { type Readable, derived, writable, get } from 'svelte/store' +import { writable, get } from 'svelte/store' -import type { PersonRoomPresence, Room, RoomPresence, MyDataItem } from './types' +import type { RoomPresence, MyDataItem } from './types' type PersonPresenceMap = Map, RoomPresence[]> @@ -31,48 +30,6 @@ export const followee = writable | undefined>(undefined) const personDataMap = new Map, Map>() const followeeDataHandlers = new Map Promise>>() -export const presenceByObjectId = derived, Map, PersonRoomPresence[]>>( - otherPresence, - ($presence) => { - const map = new Map, PersonRoomPresence[]>() - for (const [person, presences] of $presence.entries()) { - if (person === getCurrentEmployee()) continue - - presences.forEach((presence) => { - const values = map.get(presence.room.objectId) ?? [] - values.push({ person, ...presence }) - - map.set(presence.room.objectId, values) - }) - } - - return map - } -) - -export function updateMyPresence (room: Room, presence: PresenceData): void { - // lleo console.log('@@@ key: ', room, presence) - - myPresence.update((rooms) => { - const value = { room, presence, lastUpdated: Date.now() } - - const index = rooms.findIndex((it) => it.room.objectId === room.objectId) - if (index >= 0) { - rooms[index] = value - } else { - rooms.push(value) - } - - return rooms - }) -} - -export function removeMyPresence (room: Room): void { - myPresence.update((old) => { - return old.filter((it) => it.room.objectId !== room.objectId) - }) -} - export function onPersonUpdate (person: Ref, presence: RoomPresence[]): void { otherPresence.update((map) => { map.set(person, [...presence]) diff --git a/plugins/presence-resources/src/typing.ts b/plugins/presence-resources/src/typing.ts new file mode 100644 index 0000000000..926d4f27b9 --- /dev/null +++ b/plugins/presence-resources/src/typing.ts @@ -0,0 +1,74 @@ +// Copyright © 2025 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 + +import { type Employee, type Person } from '@hcengineering/contact' +import { type UnsubscribeCallback, type Callback } from '@hcengineering/hulypulse-client' +import { getMetadata } from '@hcengineering/platform' +import presentation from '@hcengineering/presentation' +import { type Doc, type Ref } from '@hcengineering/core' +import { createPulseClient } from './pulse' + +const typingDelaySeconds = 2 + +function getWorkspace (): string { + return getMetadata(presentation.metadata.WorkspaceUuid) ?? '' +} + +export interface TypingInfo { + personId: Ref + objectId: Ref +} + +export async function subscribeTyping ( + objectId: Ref, + callback: Callback +): Promise { + const client = await createPulseClient() + if (client !== undefined) { + const workspace = getWorkspace() + try { + return await client.subscribe(`${workspace}/typing/${objectId}/`, callback) + } catch (error) { + console.warn('failed to subscribe typing info:', error) + } + } + + return async () => false +} + +export async function setTyping (personId: Ref, objectId: Ref): Promise { + const client = await createPulseClient() + + if (client !== undefined) { + const workspace = getWorkspace() + const typingInfo: TypingInfo = { personId, objectId } + try { + await client.put(`${workspace}/typing/${objectId}/${personId}`, typingInfo, typingDelaySeconds) + } catch (error) { + console.warn('failed to put typing info:', error) + } + } +} + +export async function clearTyping (me: string, objectId: string): Promise { + const client = await createPulseClient() + + if (client !== undefined) { + const workspace = getWorkspace() + try { + await client.delete(`${workspace}/typing/${objectId}/${me}`) + } catch (error) { + console.warn('failed to delete typing info:', error) + } + } +}