mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
fix: use pulse for presence (#9896)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
@@ -102,14 +102,14 @@
|
||||
|
||||
async function deleteTypingInfo (): Promise<void> {
|
||||
if (!withTypingInfo) return
|
||||
clearTyping(me, object._id)
|
||||
void clearTyping(me, object._id)
|
||||
}
|
||||
|
||||
async function updateTypingInfo (): Promise<void> {
|
||||
if (!withTypingInfo) return
|
||||
|
||||
throttle.call(() => {
|
||||
setTyping(me, object._id)
|
||||
void setTyping(me, object._id)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
|
||||
@@ -14,26 +14,66 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { notEmpty, type Doc } from '@hcengineering/core'
|
||||
import { formatName } from '@hcengineering/contact'
|
||||
import { Avatar, getPersonByPersonRefStore } from '@hcengineering/contact-resources'
|
||||
import { type Class, type Doc, type Ref, notEmpty } from '@hcengineering/core'
|
||||
import { type Person, formatName, getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { Avatar, getPersonsByPersonRefs } from '@hcengineering/contact-resources'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { IconSize, tooltip, deviceOptionsStore as deviceInfo, checkAdaptiveMatching } from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
import PresenceList from './PresenceList.svelte'
|
||||
import { presenceByObjectId, followee, toggleFollowee } from '../store'
|
||||
import { PresenceInfo, subscribePresence } from '../presence'
|
||||
import { followee, toggleFollowee } from '../store'
|
||||
|
||||
export let object: Doc
|
||||
export let size: IconSize = 'small'
|
||||
export let limit: number = 4
|
||||
|
||||
$: presence = $presenceByObjectId?.get(object._id) ?? []
|
||||
$: personByRefStore = getPersonByPersonRefStore(presence.map((p) => p.person))
|
||||
$: persons = presence
|
||||
.map((it) => it.person)
|
||||
.map((p) => $personByRefStore.get(p))
|
||||
.filter(notEmpty)
|
||||
const me = getCurrentEmployee()
|
||||
|
||||
let presenceInfo = new Map<string, Ref<Person>>()
|
||||
let persons: Person[] = []
|
||||
|
||||
$: overLimit = persons.length > limit
|
||||
$: adaptive = checkAdaptiveMatching($deviceInfo.size, 'md') || overLimit
|
||||
|
||||
let unsubscribe: (() => Promise<boolean>) | undefined
|
||||
|
||||
async function updatePresence (presenceInfo: Map<string, Ref<Person>>): Promise<void> {
|
||||
const personByRef = await getPersonsByPersonRefs(Array.from(presenceInfo.values()))
|
||||
persons = presenceInfo
|
||||
.values()
|
||||
.map((p) => personByRef.get(p))
|
||||
.filter(notEmpty)
|
||||
.toArray()
|
||||
}
|
||||
|
||||
function handlePresenceInfo (key: string, value: PresenceInfo | undefined): void {
|
||||
if (value === undefined) {
|
||||
presenceInfo.delete(key)
|
||||
presenceInfo = presenceInfo
|
||||
return
|
||||
}
|
||||
|
||||
if (presenceInfo.has(key) || value.personId === me) {
|
||||
return
|
||||
}
|
||||
|
||||
presenceInfo.set(key, value.personId)
|
||||
presenceInfo = presenceInfo
|
||||
}
|
||||
|
||||
async function updatePresenceSub (objectClass: Ref<Class<Doc>>, objectId: Ref<Doc>): Promise<void> {
|
||||
await unsubscribe?.()
|
||||
presenceInfo = new Map<string, Ref<Person>>()
|
||||
unsubscribe = await subscribePresence(objectClass, objectId, handlePresenceInfo)
|
||||
}
|
||||
|
||||
$: void updatePresenceSub(object._class, object._id)
|
||||
$: void updatePresence(presenceInfo)
|
||||
onDestroy(() => {
|
||||
void unsubscribe?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if persons.length > 0}
|
||||
|
||||
@@ -14,20 +14,43 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { type Doc } from '@hcengineering/core'
|
||||
import { getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import { updateMyPresence, removeMyPresence } from '../store'
|
||||
import { updatePresence, deletePresence } from '../presence'
|
||||
|
||||
export let object: Doc
|
||||
export let presenceTtlSeconds: number = 5
|
||||
|
||||
const personId = getCurrentEmployee()
|
||||
|
||||
async function doUpdatePresence (): Promise<void> {
|
||||
const presence = { personId, objectId: object._id, objectClass: object._class }
|
||||
await updatePresence(presence, presenceTtlSeconds)
|
||||
}
|
||||
|
||||
async function doDeletePresence (object: Doc): Promise<void> {
|
||||
const presence = { personId, objectId: object._id, objectClass: object._class }
|
||||
await deletePresence(presence)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
updateMyPresence({ objectId: object._id, objectClass: object._class }, {})
|
||||
void doUpdatePresence()
|
||||
const interval = setInterval(doUpdatePresence, presenceTtlSeconds * 1000)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
void doDeletePresence(object)
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
removeMyPresence({ objectId: object._id, objectClass: object._class })
|
||||
})
|
||||
let previousObject: Doc = object
|
||||
|
||||
$: if (object !== undefined && (object._id !== previousObject._id || object._class !== previousObject._class)) {
|
||||
void doDeletePresence(previousObject)
|
||||
previousObject = object
|
||||
void doUpdatePresence()
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
|
||||
@@ -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<Resources> => ({
|
||||
|
||||
@@ -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<Person>
|
||||
objectId: Ref<Doc>
|
||||
objectClass: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
export async function subscribePresence (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
callback: Callback<PresenceInfo | undefined>
|
||||
): Promise<UnsubscribeCallback> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<HulypulseClient | undefined> {
|
||||
if (pulseclient == null) {
|
||||
const wsPulseUrl = getMetadata(presentation.metadata.PulseUrl)
|
||||
@@ -35,37 +27,6 @@ export async function createPulseClient (): Promise<HulypulseClient | undefined>
|
||||
return pulseclient
|
||||
}
|
||||
|
||||
export interface TypingInfo {
|
||||
personId: Ref<Person>
|
||||
objectId: Ref<Doc>
|
||||
}
|
||||
|
||||
export async function subscribeTyping (
|
||||
objectId: Ref<Doc>,
|
||||
callback: Callback<TypingInfo | undefined>
|
||||
): Promise<UnsubscribeCallback> {
|
||||
const workspace = getWorkspace()
|
||||
return (await pulseclient?.subscribe(`${workspace}/typing/${objectId}/`, callback)) ?? (async () => false)
|
||||
}
|
||||
|
||||
export async function setTyping (me: string, objectId: Ref<Doc>): Promise<void> {
|
||||
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
|
||||
|
||||
@@ -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<Ref<Person>, RoomPresence[]>
|
||||
|
||||
@@ -31,48 +30,6 @@ export const followee = writable<Ref<Person> | undefined>(undefined)
|
||||
const personDataMap = new Map<Ref<Person>, Map<string, any>>()
|
||||
const followeeDataHandlers = new Map<string, Set<(data: any) => Promise<void>>>()
|
||||
|
||||
export const presenceByObjectId = derived<Readable<PersonPresenceMap>, Map<Ref<Doc>, PersonRoomPresence[]>>(
|
||||
otherPresence,
|
||||
($presence) => {
|
||||
const map = new Map<Ref<Doc>, 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<Person>, presence: RoomPresence[]): void {
|
||||
otherPresence.update((map) => {
|
||||
map.set(person, [...presence])
|
||||
|
||||
@@ -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<Person>
|
||||
objectId: Ref<Doc>
|
||||
}
|
||||
|
||||
export async function subscribeTyping (
|
||||
objectId: Ref<Doc>,
|
||||
callback: Callback<TypingInfo | undefined>
|
||||
): Promise<UnsubscribeCallback> {
|
||||
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<Employee>, objectId: Ref<Doc>): Promise<void> {
|
||||
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<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user