Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 61 additions & 27 deletions plugins/chunter-resources/src/components/ChannelTypingInfo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,58 +14,92 @@
-->
<script lang="ts">
import chunter from '@hcengineering/chunter'
import { getName, getCurrentEmployee, Person } from '@hcengineering/contact'
import { type Doc, type PersonId, getCurrentAccount } from '@hcengineering/core'
import { getName, getPersonRefsBySocialIds } from '@hcengineering/contact'
import { getPersonsByPersonRefs } from '@hcengineering/contact-resources'
import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { typing } from '@hcengineering/presence-resources'
import { Doc, type Ref } from '@hcengineering/core'
import { type TypingInfo, typing } from '@hcengineering/presence-resources'
export let object: Doc
const maxTypingPersons = 3
const me = getCurrentEmployee()
const acc = getCurrentAccount()
const client = getClient()
const hierarchy = getClient().getHierarchy()
export let object: Doc
interface TypingGroup {
status: IntlString
names: string
count: number
moreCount: number
}
let typingInfo = new Map<string, Ref<Person>>()
let typingPersonsLabel: string = ''
let typingPersonsCount = 0
let moreCount: number = 0
let typingInfo = new Map<string, TypingInfo>()
let typingGroups: TypingGroup[] = []
$: void updateTypingPersons(typingInfo)
async function updateTypingPersons (typingInfo: Map<string, Ref<Person>>): Promise<void> {
const persons = await getPersonsByPersonRefs(Array.from(typingInfo.values()))
const names = Array.from(persons.values())
.map((person) => getName(hierarchy, person))
.sort((name1, name2) => name1.localeCompare(name2))
typingPersonsCount = names.length
typingPersonsLabel = names.slice(0, maxTypingPersons).join(', ')
moreCount = Math.max(names.length - maxTypingPersons, 0)
async function updateTypingPersons (typingInfo: Map<string, TypingInfo>): Promise<void> {
if (typingInfo.size === 0) {
typingGroups = []
return
}
const groupedByStatus = new Map<IntlString, PersonId[]>()
for (const info of typingInfo.values()) {
const status = info.status ?? chunter.string.IsTyping
const existing = groupedByStatus.get(status) ?? []
existing.push(info.socialId)
groupedByStatus.set(status, existing)
}
const groups: TypingGroup[] = []
for (const [status, personIds] of groupedByStatus.entries()) {
const personRefs = await getPersonRefsBySocialIds(client, personIds)
const persons = await getPersonsByPersonRefs(Object.values(personRefs))
const names = Array.from(persons.values())
.map((person) => getName(hierarchy, person))
.sort((name1, name2) => name1.localeCompare(name2))
const displayNames = names.slice(0, maxTypingPersons).join(', ')
const moreCount = Math.max(names.length - maxTypingPersons, 0)
groups.push({
status,
names: displayNames,
count: names.length,
moreCount
})
}
groups.sort((a, b) => a.status.localeCompare(b.status))
typingGroups = groups
}
function handleTyping (typing: Map<string, Ref<Person>>): void {
function handleTyping (typing: Map<string, TypingInfo>): void {
typingInfo = typing
}
</script>

<span
class="root h-4 mt-1 mb-1 ml-0-5 overflow-label"
use:typing={{
personId: me,
socialId: acc.primarySocialId,
objectId: object._id,
onTyping: handleTyping
}}
>
{#if typingPersonsLabel !== ''}
<span class="fs-bold">
{typingPersonsLabel}
</span>
{#if moreCount > 0}
<span class="ml-1"><Label label={chunter.string.AndMore} params={{ count: moreCount }} /></span>
{#each typingGroups as group, index}
<span class="fs-bold" class:ml-1={index > 0}>{group.names}</span>
{#if group.moreCount > 0}
<span class="ml-1"><Label label={chunter.string.AndMore} params={{ count: group.moreCount }} /></span>
{/if}
<span class="ml-1"><Label label={chunter.string.IsTyping} params={{ count: typingPersonsCount }} /></span>
{/if}
<span class="ml-1"><Label label={group.status} params={{ count: group.count }} /></span>
{/each}
</span>

<style>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@
import { Analytics } from '@hcengineering/analytics'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import chunter, { ChatMessage, ChunterEvents, ThreadMessage } from '@hcengineering/chunter'
import { Class, Doc, generateId, Ref, type CommitResult } from '@hcengineering/core'
import { Class, Doc, generateId, getCurrentAccount, Ref, type CommitResult } from '@hcengineering/core'
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
import { createEventDispatcher } from 'svelte'
import { getObjectId } from '@hcengineering/view-resources'
import { ThrottledCaller } from '@hcengineering/ui'
import { getSpace, editingMessageStore } from '@hcengineering/activity-resources'
import { getCurrentEmployee } from '@hcengineering/contact'

import { getChannelSpace } from '../../utils'
import ChannelTypingInfo from '../ChannelTypingInfo.svelte'
Expand Down Expand Up @@ -99,19 +98,19 @@
}
}

const me = getCurrentEmployee()
const acc = getCurrentAccount()
const throttle = new ThrottledCaller(500)

async function deleteTypingInfo (): Promise<void> {
if (!withTypingInfo) return
void clearTyping(me, object._id)
void clearTyping(acc.primarySocialId, object._id)
}

async function updateTypingInfo (): Promise<void> {
if (!withTypingInfo) return

throttle.call(() => {
void setTyping(me, object._id)
void setTyping(acc.primarySocialId, object._id)
})
}

Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "Včera v {time}",
"AndMore": "a {count} dalších",
"IsTyping": "{count, plural, =1 {píše} other {píší}}",
"IsThinking": "{count, plural, =1 {přemýšlí} other {přemýšlejí}}",
"Loading": "Načítání...",
"MessageIn": "Zpráva #{title}",
"ThreadWasDeleted": "Tato vlákno byla smazána.",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "Gestern um {time}",
"AndMore": "und {count} weitere",
"IsTyping": "{count, plural, =1 {schreibt} other {schreiben}}",
"IsThinking": "{count, plural, =1 {denkt nach} other {denken nach}}",
"Loading": "Laden...",
"MessageIn": "Nachricht #{title}",
"ThreadWasDeleted": "Dieser Thread wurde gelöscht.",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "Yesterday at {time}",
"AndMore": "and {count} more",
"IsTyping": "{count, plural, =1 {is typing} other {are typing}}...",
"IsThinking": "{count, plural, =1 {is thinking} other {are thinking}}...",
"Loading": "Loading...",
"MessageIn": "Message #{title}",
"ThreadWasRemoved": "This thread was removed.",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "Ayer a las {time}",
"AndMore": "y {count} más",
"IsTyping": "{count, plural, =1 {está escribiendo} other {están escribiendo}}",
"IsThinking": "{count, plural, =1 {está pensando} other {están pensando}}",
"Loading": "Cargando...",
"MessageIn": "Mensaje #{title}",
"ThreadWasDeleted": "Este hilo fue eliminado.",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "Hier à {time}",
"AndMore": "et {count} autres",
"IsTyping": "{count, plural, =1 {est en train d'écrire} other {écrivent}}",
"IsThinking": "{count, plural, =1 {est en train de réfléchir} other {réfléchissent}}",
"Loading": "Chargement...",
"MessageIn": "Message #{title}",
"ThreadWasDeleted": "Ce fil de discussion a été supprimé.",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "Ieri alle {time}",
"AndMore": "e {count} altri",
"IsTyping": "{count, plural, =1 {sta scrivendo} other {stanno scrivendo}}",
"IsThinking": "{count, plural, =1 {sta pensando} other {stanno pensando}}",
"Loading": "Caricamento...",
"MessageIn": "Messaggio #{title}",
"ThreadWasDeleted": "Questo thread è stato eliminato.",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "昨日・{time}",
"AndMore": "および{count}件の他",
"IsTyping": "{count, plural, =1 {入力中} other {入力中}}",
"IsThinking": "{count, plural, =1 {考え中} other {考え中}}",
"Loading": "読み込み中...",
"MessageIn": "メッセージ #{title}",
"ThreadWasDeleted": "このスレッドは削除されました。",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "Ontem às {time}",
"AndMore": "e mais {count}",
"IsTyping": "{count, plural, =1 {está digitando} other {estão digitando}}",
"IsThinking": "{count, plural, =1 {está pensando} other {estão pensando}}",
"Loading": "Carregando...",
"MessageIn": "Mensagem #{title}",
"ThreadWasDeleted": "Este thread foi excluído.",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "Вчера в {time}",
"AndMore": "и еще {count}",
"IsTyping": "{count, plural, =1 {печатает} other {печатают}}...",
"IsThinking": "{count, plural, =1 {думает} other {думают}}...",
"Loading": "Загрузка...",
"MessageIn": "Сообщение #{title}",
"ThreadWasRemoved": "Этот поток был удален.",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "Dün - {time}",
"AndMore": "ve {count} daha",
"IsTyping": "{count, plural, =1 {yazıyor} other {yazıyorlar}}...",
"IsThinking": "{count, plural, =1 {düşünüyor} other {düşünüyorlar}}...",
"Loading": "Yükleniyor...",
"MessageIn": "#{title} içinde mesaj",
"ThreadWasRemoved": "Bu başlık kaldırıldı.",
Expand Down
1 change: 1 addition & 0 deletions plugins/communication-assets/lang/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"YesterdayAt": "昨天 于 {time}",
"AndMore": "和其他 {count} 条",
"IsTyping": "{count, plural, =1 {正在输入} other {正在输入}}",
"IsThinking": "{count, plural, =1 {正在思考} other {正在思考}}",
"Loading": "加载中...",
"MessageIn": "消息 #{title}",
"ThreadWasDeleted": "此线程已被删除。",
Expand Down
112 changes: 74 additions & 38 deletions plugins/communication-resources/src/components/TypingPresenter.svelte
Original file line number Diff line number Diff line change
@@ -1,71 +1,107 @@
<!-- 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. -->
<!--
// 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.
-->
<script lang="ts">
import { getName, getCurrentEmployee, Person } from '@hcengineering/contact'
import { type PersonId, getCurrentAccount } from '@hcengineering/core'
import { getName, getPersonRefsBySocialIds } from '@hcengineering/contact'
import { getPersonsByPersonRefs } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { typing } from '@hcengineering/presence-resources'
import { type TypingInfo, typing } from '@hcengineering/presence-resources'
import { CardID } from '@hcengineering/communication-types'

import communication from '../plugin'

export let cardId: CardID

const maxTypingPersons = 3
const me = getCurrentEmployee()
const acc = getCurrentAccount()
const client = getClient()
const hierarchy = getClient().getHierarchy()

let typingInfo = new Map<string, Ref<Person>>()
let typingPersonsLabel: string = ''
let typingPersonsCount = 0
let moreCount: number = 0
interface TypingGroup {
status: IntlString
names: string
count: number
moreCount: number
}

let typingInfo = new Map<string, TypingInfo>()
let typingGroups: TypingGroup[] = []

$: void updateTypingPersons(typingInfo)

async function updateTypingPersons (typingInfo: Map<string, Ref<Person>>): Promise<void> {
const persons = await getPersonsByPersonRefs(Array.from(typingInfo.values()))
const names = Array.from(persons.values())
.map((person) => getName(hierarchy, person))
.sort((name1, name2) => name1.localeCompare(name2))
async function updateTypingPersons (typingInfo: Map<string, TypingInfo>): Promise<void> {
if (typingInfo.size === 0) {
typingGroups = []
return
}

const groupedByStatus = new Map<IntlString, PersonId[]>()
for (const info of typingInfo.values()) {
const status = info.status ?? communication.string.IsTyping
const existing = groupedByStatus.get(status) ?? []
existing.push(info.socialId)
groupedByStatus.set(status, existing)
}

const groups: TypingGroup[] = []

for (const [status, personIds] of groupedByStatus.entries()) {
const personRefs = await getPersonRefsBySocialIds(client, personIds)
const persons = await getPersonsByPersonRefs(Object.values(personRefs))
const names = Array.from(persons.values())
.map((person) => getName(hierarchy, person))
.sort((name1, name2) => name1.localeCompare(name2))

const displayNames = names.slice(0, maxTypingPersons).join(', ')
const moreCount = Math.max(names.length - maxTypingPersons, 0)

groups.push({
status,
names: displayNames,
count: names.length,
moreCount
})
}

groups.sort((a, b) => a.status.localeCompare(b.status))

typingPersonsCount = names.length
typingPersonsLabel = names.slice(0, maxTypingPersons).join(', ')
moreCount = Math.max(names.length - maxTypingPersons, 0)
typingGroups = groups
}

function handleTyping (typing: Map<string, Ref<Person>>): void {
function handleTyping (typing: Map<string, TypingInfo>): void {
typingInfo = typing
}
</script>

<span
class="root h-4 mt-1 mb-1 ml-0-5 overflow-label"
use:typing={{
personId: me,
socialId: acc.primarySocialId,
objectId: cardId,
onTyping: handleTyping
}}
>
{#if typingPersonsLabel !== ''}
<span class="fs-bold">
{typingPersonsLabel}
</span>
{#if moreCount > 0}
<span class="ml-1"><Label label={communication.string.AndMore} params={{ count: moreCount }} /></span>
{#each typingGroups as group, index}
<span class="fs-bold" class:ml-1={index > 0}>{group.names}</span>
{#if group.moreCount > 0}
<span class="ml-1"><Label label={communication.string.AndMore} params={{ count: group.moreCount }} /></span>
{/if}
<span class="ml-1"><Label label={communication.string.IsTyping} params={{ count: typingPersonsCount }} /></span>
{/if}
<span class="ml-1"><Label label={group.status} params={{ count: group.count }} /></span>
{/each}
</span>

<style>
Expand Down
Loading
Loading