diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index 67ac89d509..dfc8774d67 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -24,7 +24,8 @@ import { type ObjectChatPanel, type ThreadMessage, type ChatInfo, - type ChannelInfo + type ChannelInfo, + type TypingInfo } from '@hcengineering/chunter' import presentation from '@hcengineering/model-presentation' import contact, { type ChannelProvider as SocialChannelProvider, type Person } from '@hcengineering/contact' @@ -35,7 +36,8 @@ import { DOMAIN_MODEL, type Ref, type Timestamp, - IndexKind + IndexKind, + DOMAIN_TRANSIENT } from '@hcengineering/core' import { type Builder, @@ -56,7 +58,7 @@ import core, { TClass, TDoc, TSpace } from '@hcengineering/model-core' import notification, { TDocNotifyContext } from '@hcengineering/model-notification' import view from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' -import type { IntlString } from '@hcengineering/platform' +import { type IntlString } from '@hcengineering/platform' import { TActivityMessage } from '@hcengineering/model-activity' import { type DocNotifyContext } from '@hcengineering/notification' @@ -155,6 +157,14 @@ export class TChatInfo extends TDoc implements ChatInfo { timestamp!: Timestamp } +@Model(chunter.class.TypingInfo, core.class.Doc, DOMAIN_TRANSIENT) +export class TTypingInfo extends TDoc implements TypingInfo { + objectId!: Ref + objectClass!: Ref> + person!: Ref + lastTyping!: Timestamp +} + export function createModel (builder: Builder): void { builder.createModel( TChunterSpace, @@ -165,7 +175,8 @@ export function createModel (builder: Builder): void { TChatMessageViewlet, TObjectChatPanel, TChatInfo, - TChannelInfo + TChannelInfo, + TTypingInfo ) const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage] diff --git a/plugins/activity-resources/src/components/Activity.svelte b/plugins/activity-resources/src/components/Activity.svelte index b2f47f38a2..9fea4555e9 100644 --- a/plugins/activity-resources/src/components/Activity.svelte +++ b/plugins/activity-resources/src/components/Activity.svelte @@ -250,7 +250,11 @@ {#if isNewestFirst && showCommenInput}
- +
{/if}
{#if showCommenInput && !isNewestFirst}
- +
{/if} diff --git a/plugins/chunter-assets/lang/en.json b/plugins/chunter-assets/lang/en.json index 72121e5360..369949aaff 100644 --- a/plugins/chunter-assets/lang/en.json +++ b/plugins/chunter-assets/lang/en.json @@ -119,6 +119,8 @@ "CreatedChannelOn": "Created this channel on {date}", "ChannelMessages": "Channel messages", "JoinChannel": "Join channel", - "YouJoinedChannel": "You have been joined to channel" + "YouJoinedChannel": "You have been joined to channel", + "AndMore": "and {count} more", + "IsTyping": "{count, plural, =1 {is} other {are}} typing..." } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/es.json b/plugins/chunter-assets/lang/es.json index 9cbd0877e7..22dc858795 100644 --- a/plugins/chunter-assets/lang/es.json +++ b/plugins/chunter-assets/lang/es.json @@ -119,6 +119,8 @@ "CreatedChannelOn": "Creó este canal el {date}", "ChannelMessages": "Mensajes del canal", "JoinChannel": "Unirse", - "YouJoinedChannel": "Te has unido al canal" + "YouJoinedChannel": "Te has unido al canal", + "AndMore": "y {count} más", + "IsTyping": "está escribiendo..." } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/fr.json b/plugins/chunter-assets/lang/fr.json index fb42dd0389..17f41e3cd7 100644 --- a/plugins/chunter-assets/lang/fr.json +++ b/plugins/chunter-assets/lang/fr.json @@ -119,6 +119,8 @@ "CreatedChannelOn": "A créé ce canal le {date}", "ChannelMessages": "Messages du canal", "JoinChannel": "Rejoindre", - "YouJoinedChannel": "Vous avez rejoint le canal" + "YouJoinedChannel": "Vous avez rejoint le canal", + "AndMore": "et {count} de plus", + "IsTyping": "est en train d'écrire..." } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/pt.json b/plugins/chunter-assets/lang/pt.json index 3758587fdf..0d658d13a2 100644 --- a/plugins/chunter-assets/lang/pt.json +++ b/plugins/chunter-assets/lang/pt.json @@ -119,6 +119,8 @@ "CreatedChannelOn": "Criou este canal em {date}", "ChannelMessages": "Mensagens do canal", "JoinChannel": "Participar no canal", - "YouJoinedChannel": "Entrou no canal" + "YouJoinedChannel": "Entrou no canal", + "AndMore": "e mais {count}", + "IsTyping": "está a escrever..." } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/ru.json b/plugins/chunter-assets/lang/ru.json index f18301ff30..ff275c5510 100644 --- a/plugins/chunter-assets/lang/ru.json +++ b/plugins/chunter-assets/lang/ru.json @@ -119,6 +119,8 @@ "CreatedChannelOn": "Создал этот канал {date}", "ChannelMessages": "Сообщения каналов", "JoinChannel": "Приссоединение к каналу", - "YouJoinedChannel": "Вы присоединились к каналу" + "YouJoinedChannel": "Вы присоединились к каналу", + "AndMore": "и еще {count}", + "IsTyping": "{count, plural, =1 {печатает} other {печатают}}..." } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/zh.json b/plugins/chunter-assets/lang/zh.json index 0da4296619..5365aaac63 100644 --- a/plugins/chunter-assets/lang/zh.json +++ b/plugins/chunter-assets/lang/zh.json @@ -119,6 +119,8 @@ "CreatedChannelOn": "于 {date} 创建此频道", "ChannelMessages": "频道消息", "JoinChannel": "加入频道", - "YouJoinedChannel": "你已加入频道" + "YouJoinedChannel": "你已加入频道", + "AndMore": "和 {count} 人", + "IsTyping": "正在输入..." } } diff --git a/plugins/chunter-resources/src/components/Channel.svelte b/plugins/chunter-resources/src/components/Channel.svelte index 966c148705..1a3e973538 100644 --- a/plugins/chunter-resources/src/components/Channel.svelte +++ b/plugins/chunter-resources/src/components/Channel.svelte @@ -98,8 +98,6 @@ {#if dataProvider} > - export let objectId: Ref + export let object: Doc export let selectedMessageId: Ref | undefined = undefined export let scrollElement: HTMLDivElement | undefined | null = undefined export let startFromBottom = false @@ -115,9 +113,9 @@ $: messages = $messagesStore $: isLoading = $isLoadingStore - $: extensions = client.getModel().findAllSync(activity.class.ActivityExtension, { ofClass: objectClass }) + $: extensions = client.getModel().findAllSync(activity.class.ActivityExtension, { ofClass: doc._class }) - $: notifyContext = $contextByDocStore.get(objectId) + $: notifyContext = $contextByDocStore.get(doc._id) void client .getModel() @@ -129,7 +127,7 @@ } }) - $: displayMessages = filterChatMessages(messages, filters, filterResources, objectClass, selectedFilters) + $: displayMessages = filterChatMessages(messages, filters, filterResources, doc._class, selectedFilters) const unsubscribe = inboxClient.inboxNotificationsByContext.subscribe(() => { if (notifyContext !== undefined) { @@ -674,7 +672,7 @@ } const op = client.apply(generateId(), 'chunter.scrollDown') - await inboxClient.readDoc(op, objectId) + await inboxClient.readDoc(op, doc._id) await op.commit() } @@ -693,7 +691,7 @@ if (unViewed.length === 0) { forceRead = true const op = client.apply(generateId(), 'chunter.forceReadContext') - await inboxClient.readDoc(op, objectId) + await inboxClient.readDoc(op, object._id) await op.commit() } } @@ -790,7 +788,7 @@
{/if} @@ -805,6 +803,7 @@ .ref-input { flex-shrink: 0; margin: 1.25rem 1rem 1rem; + margin-bottom: 0; max-height: 18.75rem; } diff --git a/plugins/chunter-resources/src/components/ChannelTypingInfo.svelte b/plugins/chunter-resources/src/components/ChannelTypingInfo.svelte new file mode 100644 index 0000000000..29e4f3aedd --- /dev/null +++ b/plugins/chunter-resources/src/components/ChannelTypingInfo.svelte @@ -0,0 +1,83 @@ + + + + + {#if typingPersonsLabel !== ''} + + {typingPersonsLabel} + + {#if moreCount > 0} + + {/if} + + {/if} + + + diff --git a/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte b/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte index cbcbb100ca..76ea0a5b70 100644 --- a/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte +++ b/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte @@ -16,14 +16,18 @@ import activity, { ActivityMessage } from '@hcengineering/activity' 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 { createQuery, DraftController, draftsStore, getClient, isSpace } from '@hcengineering/presentation' - import { EmptyMarkup } from '@hcengineering/text' + import chunter, { ChatMessage, ChunterEvents, ThreadMessage, TypingInfo } from '@hcengineering/chunter' + import { Class, Doc, generateId, Ref, type CommitResult, getCurrentAccount } 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 } from '@hcengineering/activity-resources' + import { PersonAccount } from '@hcengineering/contact' import { getChannelSpace } from '../../utils' + import ChannelTypingInfo from '../ChannelTypingInfo.svelte' export let object: Doc export let chatMessage: ChatMessage | undefined = undefined @@ -33,6 +37,7 @@ export let loading = false export let collection: string = 'comments' export let autofocus = false + export let withTypingInfo = false type MessageDraft = Pick @@ -60,20 +65,26 @@ let inputContent = currentMessage.message $: if (currentDraft != null) { - createdMessageQuery.query( - _class, - { _id, space: getChannelSpace(object._class, object._id, object.space) }, - (result: ChatMessage[]) => { - if (result.length > 0 && _id !== chatMessage?._id) { - // Ouch we have got comment with same id created already. - clear() - } + createdMessageQuery.query(_class, { _id, space: getSpace(object) }, (result: ChatMessage[]) => { + if (result.length > 0 && _id !== chatMessage?._id) { + // Ouch we have got comment with same id created already. + clear() } - ) + }) } else { createdMessageQuery.unsubscribe() } + const typingInfoQuery = createQuery() + let typingInfo: TypingInfo[] = [] + $: if (withTypingInfo) { + typingInfoQuery.query(chunter.class.TypingInfo, { objectId: object._id, space: getSpace(object) }, (res) => { + typingInfo = res + }) + } else { + typingInfoQuery.unsubscribe() + } + function clear (): void { currentMessage = getDefault() _id = currentMessage._id @@ -95,7 +106,40 @@ } } + const me = getCurrentAccount() as PersonAccount + const throttle = new ThrottledCaller(500) + + async function deleteTypingInfo (): Promise { + if (!withTypingInfo) return + const myTypingInfo = typingInfo.find((info) => info.person === me.person) + if (myTypingInfo === undefined) return + await client.remove(myTypingInfo) + } + + async function updateTypingInfo (): Promise { + if (!withTypingInfo) return + const myTypingInfo = typingInfo.find((info) => info.person === me.person) + + if (myTypingInfo === undefined) { + await client.createDoc(chunter.class.TypingInfo, getSpace(object), { + objectId: object._id, + objectClass: object._class, + person: me.person, + lastTyping: Date.now() + }) + } else { + throttle.call(() => { + void client.update(myTypingInfo, { + lastTyping: Date.now() + }) + }) + } + } + function onUpdate (event: CustomEvent): void { + if (!isEmptyMarkup(event.detail.message)) { + void updateTypingInfo() + } if (!shouldSaveDraft) { return } @@ -141,6 +185,7 @@ await handleEdit(event) } else { void handleCreate(event, _id) + void deleteTypingInfo() } // Remove draft from Local Storage @@ -173,7 +218,7 @@ } else { await operations.addCollection( _class, - isSpace(object) ? object._id : object.space, + getSpace(object), object._id, object._class, collection, @@ -213,3 +258,7 @@ on:blur bind:loading /> + +{#if withTypingInfo} + +{/if} diff --git a/plugins/chunter-resources/src/components/threads/ThreadView.svelte b/plugins/chunter-resources/src/components/threads/ThreadView.svelte index a5afb44a60..cf9b4713e1 100644 --- a/plugins/chunter-resources/src/components/threads/ThreadView.svelte +++ b/plugins/chunter-resources/src/components/threads/ThreadView.svelte @@ -118,15 +118,7 @@
{#if message && dataProvider !== undefined} - +
diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts index 93137eec92..67f6d08efe 100644 --- a/plugins/chunter/src/index.ts +++ b/plugins/chunter/src/index.ts @@ -80,6 +80,13 @@ export interface ChatInfo extends Doc { timestamp: Timestamp } +export interface TypingInfo extends Doc { + objectId: Ref + objectClass: Ref> + person: Ref + lastTyping: Timestamp +} + export interface ChannelInfo extends DocNotifyContext { hidden: boolean } @@ -124,7 +131,8 @@ export default plugin(chunterId, { DirectMessage: '' as Ref>, ChatMessage: '' as Ref>, ChatMessageViewlet: '' as Ref>, - ChatInfo: '' as Ref> + ChatInfo: '' as Ref>, + TypingInfo: '' as Ref> }, mixin: { ObjectChatPanel: '' as Ref>, @@ -179,7 +187,9 @@ export default plugin(chunterId, { Added: '' as IntlString, Removed: '' as IntlString, CreatedChannelOn: '' as IntlString, - YouJoinedChannel: '' as IntlString + YouJoinedChannel: '' as IntlString, + AndMore: '' as IntlString, + IsTyping: '' as IntlString }, ids: { DMNotification: '' as Ref,