fix: refactor presence and typing to use svelte actions (#9908)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2025-09-22 23:18:22 +07:00
committed by GitHub
parent 3237890e6a
commit 9ae9453a52
5 changed files with 182 additions and 122 deletions
@@ -13,13 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { onDestroy } from 'svelte'
import chunter from '@hcengineering/chunter'
import { getName, getCurrentEmployee, Person } from '@hcengineering/contact'
import { getPersonsByPersonRefs } from '@hcengineering/contact-resources'
import { getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { subscribeTyping, TypingInfo } from '@hcengineering/presence-resources'
import { typing } from '@hcengineering/presence-resources'
import { Doc, type Ref } from '@hcengineering/core'
const maxTypingPersons = 3
@@ -45,37 +44,19 @@
moreCount = Math.max(names.length - maxTypingPersons, 0)
}
function handleTypingInfo (key: string, value: TypingInfo | undefined): void {
if (value === undefined) {
typingInfo.delete(key)
typingInfo = typingInfo
return
}
if (typingInfo.has(key) || value.personId === me) {
return
}
typingInfo.set(key, value.personId)
typingInfo = typingInfo
function handleTyping (typing: Map<string, Ref<Person>>): void {
typingInfo = typing
}
let unsubscribe: (() => Promise<boolean>) | undefined
async function updateTypingSub (objectId: Ref<Doc>): Promise<void> {
await unsubscribe?.()
typingInfo = new Map<string, Ref<Person>>()
unsubscribe = await subscribeTyping(objectId, handleTypingInfo)
}
$: void updateTypingSub(object._id)
onDestroy(() => {
void unsubscribe?.()
})
</script>
<span class="root h-4 mt-1 mb-1 ml-0-5 overflow-label">
<span
class="root h-4 mt-1 mb-1 ml-0-5 overflow-label"
use:typing={{
personId: me,
objectId: object._id,
onTyping: handleTyping
}}
>
{#if typingPersonsLabel !== ''}
<span class="fs-bold">
{typingPersonsLabel}
@@ -11,13 +11,12 @@
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { onDestroy } from 'svelte'
import { getName, getCurrentEmployee, Person } from '@hcengineering/contact'
import { getPersonsByPersonRefs } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { subscribeTyping, TypingInfo } from '@hcengineering/presence-resources'
import { typing } from '@hcengineering/presence-resources'
import { CardID } from '@hcengineering/communication-types'
import communication from '../plugin'
@@ -45,37 +44,19 @@
moreCount = Math.max(names.length - maxTypingPersons, 0)
}
function handleTypingInfo (key: string, value: TypingInfo | undefined): void {
if (value === undefined) {
typingInfo.delete(key)
typingInfo = typingInfo
return
}
if (typingInfo.has(key) || value.personId === me) {
return
}
typingInfo.set(key, value.personId)
typingInfo = typingInfo
function handleTyping (typing: Map<string, Ref<Person>>): void {
typingInfo = typing
}
let unsubscribe: (() => Promise<boolean>) | undefined
async function updateTypingSub (cardId: CardID): Promise<void> {
await unsubscribe?.()
typingInfo = new Map<string, Ref<Person>>()
unsubscribe = await subscribeTyping(cardId, handleTypingInfo)
}
$: void updateTypingSub(cardId)
onDestroy(() => {
void unsubscribe?.()
})
</script>
<span class="root h-4 mt-1 mb-1 ml-0-5 overflow-label">
<span
class="root h-4 mt-1 mb-1 ml-0-5 overflow-label"
use:typing={{
personId: me,
objectId: cardId,
onTyping: handleTyping
}}
>
{#if typingPersonsLabel !== ''}
<span class="fs-bold">
{typingPersonsLabel}
@@ -14,15 +14,14 @@
-->
<script lang="ts">
import { type Class, type Doc, type Ref, notEmpty } from '@hcengineering/core'
import { 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 { PresenceInfo, subscribePresence } from '../presence'
import { presence } from '../presence'
import { followee, toggleFollowee } from '../store'
export let object: Doc
@@ -37,8 +36,6 @@
$: 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
@@ -48,68 +45,56 @@
.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
function onPresence (presence: Map<string, Ref<Person>>): void {
presenceInfo = presence
}
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}
{#if adaptive}
<div
class="hulyCombineAvatars-container"
use:tooltip={{ component: PresenceList, props: { persons, size }, direction: 'bottom' }}
>
{#each persons.slice(0, limit) as person, i}
<div
class="hulyCombineAvatar tiny"
data-over={i === limit - 1 && overLimit ? `+${persons.length - limit + 1}` : undefined}
>
<Avatar name={person.name} {size} {person} />
</div>
{/each}
</div>
{:else}
<div class="flex-row-center flex-gap-1">
{#each persons as person}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
use:tooltip={{ label: getEmbeddedLabel(formatName(person.name)) }}
class="avatar-button"
class:followee-avatar={$followee === person._id}
on:click={() => {
toggleFollowee(person._id)
}}
>
<Avatar name={person.name} {size} {person} />
</div>
{/each}
</div>
<div
use:presence={{
personId: me,
objectId: object._id,
objectClass: object._class,
onPresence
}}
>
{#if persons.length > 0}
{#if adaptive}
<div
class="hulyCombineAvatars-container"
use:tooltip={{ component: PresenceList, props: { persons, size }, direction: 'bottom' }}
>
{#each persons.slice(0, limit) as person, i}
<div
class="hulyCombineAvatar tiny"
data-over={i === limit - 1 && overLimit ? `+${persons.length - limit + 1}` : undefined}
>
<Avatar name={person.name} {size} {person} />
</div>
{/each}
</div>
{:else}
<div class="flex-row-center flex-gap-1">
{#each persons as person}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
use:tooltip={{ label: getEmbeddedLabel(formatName(person.name)) }}
class="avatar-button"
class:followee-avatar={$followee === person._id}
on:click={() => {
toggleFollowee(person._id)
}}
>
<Avatar name={person.name} {size} {person} />
</div>
{/each}
</div>
{/if}
{/if}
{/if}
</div>
<style lang="scss">
.avatar-button {
+59 -1
View File
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License
import { type Person } from '@hcengineering/contact'
import { type Employee, 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'
@@ -25,6 +25,64 @@ export interface PresenceInfo {
objectClass: Ref<Class<Doc>>
}
export interface PresenceActionParams {
personId: Ref<Employee>
objectId: Ref<Doc>
objectClass: Ref<Class<Doc>>
onPresence: (presence: Map<string, Ref<Person>>) => void
}
export function presence (node: HTMLElement, params: PresenceActionParams): any {
let unsubscribe: Promise<UnsubscribeCallback> | undefined
let presence = new Map<string, Ref<Person>>()
let personId = params.personId
let objectId = params.objectId
let objectClass = params.objectClass
let onPresence = params.onPresence
function handlePresenceInfo (key: string, value: PresenceInfo | undefined): void {
if (value?.personId === personId) {
return
}
if (value === undefined) {
presence.delete(key)
} else {
presence.set(key, value.personId)
}
onPresence(presence)
}
unsubscribe = subscribePresence(params.objectClass, params.objectId, handlePresenceInfo)
return {
update: (params: PresenceActionParams) => {
if (objectId !== params.objectId || objectClass !== params.objectClass) {
personId = params.personId
objectId = params.objectId
objectClass = params.objectClass
onPresence = params.onPresence
void unsubscribe?.then((unsub) => {
void unsub()
})
presence = new Map<string, Ref<Person>>()
unsubscribe = subscribePresence(params.objectClass, params.objectId, handlePresenceInfo)
onPresence(presence)
}
},
destroy: () => {
void unsubscribe?.then((unsub) => {
void unsub()
})
}
}
}
export async function subscribePresence (
objectClass: Ref<Class<Doc>>,
objectId: Ref<Doc>,
+55
View File
@@ -29,6 +29,61 @@ export interface TypingInfo {
objectId: Ref<Doc>
}
export interface TypingActionParams {
personId: Ref<Employee>
objectId: Ref<Doc>
onTyping: (presence: Map<string, Ref<Person>>) => void
}
export function typing (node: HTMLElement, params: TypingActionParams): any {
let unsubscribe: Promise<UnsubscribeCallback> | undefined
let presence = new Map<string, Ref<Person>>()
let personId = params.personId
let objectId = params.objectId
let onTyping = params.onTyping
function handleTypingInfo (key: string, value: TypingInfo | undefined): void {
if (value?.personId === personId) {
return
}
if (value === undefined) {
presence.delete(key)
} else {
presence.set(key, value.personId)
}
onTyping(presence)
}
unsubscribe = subscribeTyping(params.objectId, handleTypingInfo)
return {
update: (params: TypingActionParams) => {
if (objectId !== params.objectId) {
personId = params.personId
objectId = params.objectId
onTyping = params.onTyping
void unsubscribe?.then((unsub) => {
void unsub()
})
presence = new Map<string, Ref<Person>>()
unsubscribe = subscribeTyping(params.objectId, handleTypingInfo)
onTyping(presence)
}
},
destroy: () => {
void unsubscribe?.then((unsub) => {
void unsub()
})
}
}
}
export async function subscribeTyping (
objectId: Ref<Doc>,
callback: Callback<TypingInfo | undefined>