diff --git a/plugins/presence-resources/src/client.ts b/plugins/presence-resources/src/client.ts index 2198d5b72a..5045459854 100644 --- a/plugins/presence-resources/src/client.ts +++ b/plugins/presence-resources/src/client.ts @@ -20,10 +20,10 @@ import presence from '@hcengineering/presence' import presentation from '@hcengineering/presentation' import { type Unsubscriber, get } from 'svelte/store' -import { myPresence, myData, onPersonUpdate, onPersonLeave, onPersonData } from './store' +import { myPresence, myData, onPersonUpdate, onPersonLeave, onPersonData, followee, toggleFollowee } from './store' import type { RoomPresence, MyDataItem } from './types' -interface Message { +interface PresenceMessage { id: Ref type: 'update' | 'remove' presence?: RoomPresence[] @@ -31,12 +31,31 @@ interface Message { } interface DataMessage { - id: Ref type: 'data' - key: string + sender: Ref + topic: string data: any } +interface FollowMessage { + type: 'follow' + follower: Ref + followee: Ref +} + +interface UnfollowMessage { + type: 'unfollow' + follower: Ref +} + +interface FollowedMessage { + type: 'followed' + follower: Ref + active: boolean +} + +type IncomingMessage = PresenceMessage | DataMessage | FollowedMessage | UnfollowMessage + export class PresenceClient implements Disposable { private ws: WebSocket | null = null private closed = false @@ -48,6 +67,8 @@ export class PresenceClient implements Disposable { private readonly myDataTimestamps = new Map() private readonly myPresenceUnsub: Unsubscriber private readonly myDataUnsub: Unsubscriber + private readonly followeeUnsub: Unsubscriber + private readonly followers = new Set>() constructor (private readonly url: string | URL) { this.presence = get(myPresence) @@ -55,7 +76,10 @@ export class PresenceClient implements Disposable { this.handlePresenceChanged(presence) }) this.myDataUnsub = myData.subscribe((data) => { - this.handleMyDataChanged(data) + this.handleMyDataChanged(data, false) + }) + this.followeeUnsub = followee.subscribe((followee) => { + this.handleFolloweeChanged(followee) }) this.connect() @@ -67,6 +91,7 @@ export class PresenceClient implements Disposable { this.myPresenceUnsub() this.myDataUnsub() + this.followeeUnsub() if (this.ws !== null) { this.ws.close() @@ -128,18 +153,28 @@ export class PresenceClient implements Disposable { private handleConnect (): void { this.sendPresence(getCurrentEmployee(), this.presence) - this.sendMyData(getCurrentEmployee(), get(myData)) + + this.handleMyDataChanged(get(myData), true) + + const f = get(followee) + if (f !== undefined) { + this.handleFolloweeChanged(f) + } } private handleMessage (data: string): void { try { - const message = JSON.parse(data) as Message | DataMessage + const message = JSON.parse(data) as IncomingMessage if (message.type === 'update' && message.presence !== undefined) { onPersonUpdate(message.id, message.presence ?? []) } else if (message.type === 'remove') { onPersonLeave(message.id) } else if (message.type === 'data') { - onPersonData(message.id, message.key, message.data) + onPersonData(message.sender, message.topic, message.data) + } else if (message.type === 'followed') { + this.onFollowed(message.follower, message.active) + } else if (message.type === 'unfollow') { + toggleFollowee(undefined) } else { console.warn('Unknown message type', message) } @@ -155,28 +190,60 @@ export class PresenceClient implements Disposable { private sendPresence (person: Ref, presence: RoomPresence[]): void { if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) { - const message: Message = { id: person, type: 'update', presence } + const message: PresenceMessage = { id: person, type: 'update', presence } this.ws.send(JSON.stringify(message)) } } - private handleMyDataChanged (data: Map): void { - this.sendMyData(getCurrentEmployee(), data) + private handleMyDataChanged (data: Map, forceSend: boolean): void { + if (this.followers.size === 0) { + return + } + if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) { + for (const [topic, value] of data) { + const lastSend = this.myDataTimestamps.get(topic) ?? 0 + if (value.lastUpdated >= lastSend + this.myDataThrottleInterval || forceSend) { + this.myDataTimestamps.set(topic, value.lastUpdated) + const message: DataMessage = { + sender: getCurrentEmployee(), + type: 'data', + topic, + data: value.data + } + this.ws.send(JSON.stringify(message)) + } + } + } } - private sendMyData (person: Ref, data: Map): void { + private handleFolloweeChanged (followee: Ref | undefined): void { if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) { - for (const [key, value] of data) { - const lastSend = this.myDataTimestamps.get(key) ?? 0 - if (value.lastUpdated >= lastSend + this.myDataThrottleInterval || value.forceSend) { - this.myDataTimestamps.set(key, value.lastUpdated) - const message: DataMessage = { id: person, type: 'data', key, data: value.data } - this.ws.send(JSON.stringify(message)) + if (followee !== undefined) { + const message: FollowMessage = { + type: 'follow', + follower: getCurrentEmployee(), + followee + } + this.ws.send(JSON.stringify(message)) + } else { + const message: UnfollowMessage = { + type: 'unfollow', + follower: getCurrentEmployee() } + this.ws.send(JSON.stringify(message)) } } } + private onFollowed (follower: Ref, active: boolean): void { + if (active) { + this.followers.add(follower) + this.handleMyDataChanged(get(myData), true) + } else { + this.followers.delete(follower) + } + } + [Symbol.dispose] (): void { this.close() } diff --git a/plugins/presence-resources/src/index.ts b/plugins/presence-resources/src/index.ts index 638a4d5eab..59c0248ae9 100644 --- a/plugins/presence-resources/src/index.ts +++ b/plugins/presence-resources/src/index.ts @@ -18,7 +18,7 @@ import { type Resources } from '@hcengineering/platform' import Presence from './components/Presence.svelte' import PresenceAvatars from './components/PresenceAvatars.svelte' import WorkbenchExtension from './components/WorkbenchExtension.svelte' -import { getFollowee, sendMyData, subscribeToOtherData, unsubscribeFromOtherData } from './store' +import { getFollowee, publishData, followeeDataSubscribe, followeeDataUnsubscribe } from './store' export { Presence, PresenceAvatars } export { updateMyPresence, removeMyPresence, presenceByObjectId } from './store' @@ -32,9 +32,9 @@ export default async (): Promise => ({ WorkbenchExtension }, function: { - SendMyData: sendMyData, + PublishData: publishData, GetFollowee: getFollowee, - SubscribeToOtherData: subscribeToOtherData, - UnsubscribeFromOtherData: unsubscribeFromOtherData + FolloweeDataSubscribe: followeeDataSubscribe, + FolloweeDataUnsubscribe: followeeDataUnsubscribe } }) diff --git a/plugins/presence-resources/src/store.ts b/plugins/presence-resources/src/store.ts index cfaf864a17..dd140cc861 100644 --- a/plugins/presence-resources/src/store.ts +++ b/plugins/presence-resources/src/store.ts @@ -28,8 +28,8 @@ export const myData = writable>(new Map()) export const otherPresence = writable(new Map()) export const followee = writable | undefined>(undefined) -const otherDataMap = new Map, Map>() -const otherDataHandlers = new Map void>>() +const personDataMap = new Map, Map>() +const followeeDataHandlers = new Map void>>() export const presenceByObjectId = derived, Map, PersonRoomPresence[]>>( otherPresence, @@ -86,15 +86,15 @@ export function onPersonLeave (person: Ref): void { }) } -export function onPersonData (person: Ref, key: string, data: any): void { - const otherData = otherDataMap.get(person) - if (otherData !== undefined) { - otherData.set(key, data) +export function onPersonData (person: Ref, topic: string, data: any): void { + const personData = personDataMap.get(person) + if (personData !== undefined) { + personData.set(topic, data) } else { - otherDataMap.set(person, new Map([[key, data]])) + personDataMap.set(person, new Map([[topic, data]])) } if (person === get(followee)) { - const handlers = otherDataHandlers.get(key) + const handlers = followeeDataHandlers.get(topic) if (handlers !== undefined) { for (const handler of handlers) { handler(data) @@ -103,52 +103,52 @@ export function onPersonData (person: Ref, key: string, data: any): void } } -export function subscribeToOtherData (key: string, callback: (data: any) => void): void { - const handlers = otherDataHandlers.get(key) +export function followeeDataSubscribe (topic: string, handler: (data: any) => void): void { + const handlers = followeeDataHandlers.get(topic) if (handlers !== undefined) { - handlers.add(callback) + handlers.add(handler) } else { - otherDataHandlers.set(key, new Set([callback])) + followeeDataHandlers.set(topic, new Set([handler])) } - const p = get(followee) - if (p !== undefined) { - const otherData = otherDataMap.get(p) - if (otherData !== undefined) { - const data = otherData.get(key) + const f = get(followee) + if (f !== undefined) { + const followeeData = personDataMap.get(f) + if (followeeData !== undefined) { + const data = followeeData.get(topic) if (data !== undefined) { - callback(data) + handler(data) } } } } -export function unsubscribeFromOtherData (key: string, callback: (data: any) => void): void { - const handlers = otherDataHandlers.get(key) +export function followeeDataUnsubscribe (topic: string, handler: (data: any) => void): void { + const handlers = followeeDataHandlers.get(topic) if (handlers !== undefined) { - handlers.delete(callback) + handlers.delete(handler) } } -export function toggleFollowee (person: Ref): void { +export function toggleFollowee (person: Ref | undefined): void { followee.update((p) => (p === person ? undefined : person)) - const p = get(followee) - if (p !== undefined) { - const otherData = otherDataMap.get(p) + const f = get(followee) + if (f !== undefined) { + const otherData = personDataMap.get(f) if (otherData !== undefined) { - for (const [key, data] of otherData) { - const handlers = otherDataHandlers.get(key) + for (const [topic, data] of otherData) { + const handlers = followeeDataHandlers.get(topic) if (handlers !== undefined) { - for (const callback of handlers) { - callback(data) + for (const handler of handlers) { + handler(data) } } } } } else { - for (const handlers of otherDataHandlers.values()) { - for (const callback of handlers) { - callback(undefined) + for (const handlers of followeeDataHandlers.values()) { + for (const handler of handlers) { + handler(undefined) } } } @@ -163,9 +163,9 @@ export function getFollowee (): Person | undefined { return personMap.get(followeeId) } -export function sendMyData (key: string, data: any, forceSend: boolean = false): void { +export function publishData (topic: string, data: any): void { myData.update((map) => { - map.set(key, { lastUpdated: Date.now(), data, forceSend }) + map.set(topic, { lastUpdated: Date.now(), data }) return map }) } diff --git a/plugins/presence-resources/src/types.ts b/plugins/presence-resources/src/types.ts index 33c929b9a3..0a04acce3c 100644 --- a/plugins/presence-resources/src/types.ts +++ b/plugins/presence-resources/src/types.ts @@ -44,5 +44,4 @@ export interface PersonPresence { export interface MyDataItem { data: any lastUpdated: number - forceSend: boolean } diff --git a/plugins/presence/src/plugin.ts b/plugins/presence/src/plugin.ts index 87b0ac2ed3..8fb11e44a3 100644 --- a/plugins/presence/src/plugin.ts +++ b/plugins/presence/src/plugin.ts @@ -30,10 +30,10 @@ export const presencePlugin = plugin(presenceId, { PresenceAvatars: '' as AnyComponent }, function: { - SendMyData: '' as Resource<(key: string, data: any) => void>, + PublishData: '' as Resource<(topic: string, data: any) => void>, GetFollowee: '' as Resource<() => Person | undefined>, - SubscribeToOtherData: '' as Resource<(key: string, callback: (data: any) => void) => void>, - UnsubscribeFromOtherData: '' as Resource<(key: string, callback: (data: any) => void) => void> + FolloweeDataSubscribe: '' as Resource<(topic: string, handler: (data: any) => void) => void>, + FolloweeDataUnsubscribe: '' as Resource<(topic: string, handler: (data: any) => void) => void> } }) diff --git a/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte b/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte index 1a6b2a7cd0..5692bfb9e7 100644 --- a/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte +++ b/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte @@ -55,11 +55,12 @@ let toolbar: HTMLDivElement let oldSelected = false let oldReadonly = false - let sendLiveData: ((key: string, data: any, force: boolean) => void) | undefined + let sendLiveData: ((key: string, data: any) => void) | undefined let getFollowee: (() => Person | undefined) | undefined let panning = false let followee: Person | undefined - const dataKey = 'drawing-board' + const dataTopicOffset = 'drawing-board-offset' + const dataTopicCursor = 'drawing-board-cursor' $: onSelectedChanged(selected) $: onReadonlyChanged(readonly) @@ -144,17 +145,17 @@ function onOffsetChanged (offset: Point): void { if (sendLiveData !== undefined) { - sendLiveData(dataKey, { boardId, offset: { ...offset } }, true) + sendLiveData(dataTopicOffset, { boardId, offset: { ...offset } }) } } function onPointerMoved (canvasPos: Point): void { if (sendLiveData !== undefined && selected && !panning) { - sendLiveData(dataKey, { boardId, cursorPos: { ...canvasPos } }, false) + sendLiveData(dataTopicCursor, { boardId, cursorPos: { ...canvasPos } }) } } - function onLiveData (data: any): void { + function onFolloweeData (data: any): void { if (data === undefined) { followee = undefined personCursorVisible = false @@ -180,12 +181,12 @@ commands = savedCmds.toArray() savedCmds.observe(listenSavedCommands) - getResource(presence.function.SendMyData) + getResource(presence.function.PublishData) .then((func) => { sendLiveData = func }) .catch((err) => { - console.error('Failed to get presence.function.SendMyData', err) + console.error('Failed to get presence.function.PublishData', err) }) getResource(presence.function.GetFollowee) .then((func) => { @@ -194,24 +195,26 @@ .catch((err) => { console.error('Failed to get presence.function.GetFollowee', err) }) - getResource(presence.function.SubscribeToOtherData) + getResource(presence.function.FolloweeDataSubscribe) .then((subscribe) => { - subscribe(dataKey, onLiveData) + subscribe(dataTopicOffset, onFolloweeData) + subscribe(dataTopicCursor, onFolloweeData) }) .catch((err) => { - console.error('Failed to get presence.function.SubscribeToOtherData', err) + console.error('Failed to get presence.function.FolloweeDataSubscribe', err) }) }) onDestroy(() => { savedCmds.unobserve(listenSavedCommands) - getResource(presence.function.UnsubscribeFromOtherData) + getResource(presence.function.FolloweeDataUnsubscribe) .then((unsubscribe) => { - unsubscribe(dataKey, onLiveData) + unsubscribe(dataTopicOffset, onFolloweeData) + unsubscribe(dataTopicCursor, onFolloweeData) }) .catch((err) => { - console.error('failed to get presence.function.UnsubscribeFromOtherData', err) + console.error('failed to get presence.function.FolloweeDataUnsubscribe', err) }) })