From c7a60c524dadeb946ebd2499f040242a7ba00e44 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Wed, 30 Sep 2026 13:01:42 +0330 Subject: [PATCH] Editing correction (in progress) --- .../conversations/conversations.controller.ts | 4 ++ .../conversations/conversations.service.ts | 55 +++++++++++++++ apps/web/src/lib/useOrgChat.ts | 67 +++++++++++++------ apps/web/src/views/ConversationsView.vue | 32 ++++++++- 4 files changed, 135 insertions(+), 23 deletions(-) diff --git a/apps/api/src/conversations/conversations.controller.ts b/apps/api/src/conversations/conversations.controller.ts index 1206268..11e226f 100644 --- a/apps/api/src/conversations/conversations.controller.ts +++ b/apps/api/src/conversations/conversations.controller.ts @@ -62,6 +62,10 @@ export class ConversationsController { @CurrentUser() user: { userId: string; organizationId: string; role: string }, @Query('externalRoomId') externalRoomId: string, ) { + // بدون roomId → همه اتاق‌های سازمان (لیست گفتگو) + if (!externalRoomId?.trim()) { + return this.conversations.listTypingAll(user.userId, user.organizationId, user.role); + } return this.conversations.listTyping( user.userId, user.organizationId, diff --git a/apps/api/src/conversations/conversations.service.ts b/apps/api/src/conversations/conversations.service.ts index b0ad100..da5dadf 100644 --- a/apps/api/src/conversations/conversations.service.ts +++ b/apps/api/src/conversations/conversations.service.ts @@ -98,6 +98,61 @@ export class ConversationsService { })); } + /** typing همه اتاق‌های سازمان (برای پیش‌نمایش لیست گفتگو) */ + async listTypingAll( + userId: string, + organizationId: string, + role: string, + ): Promise< + Array<{ externalRoomId: string; userId: string; fullName: string; chatUserId: string }> + > { + await this.loadAccess(userId, organizationId, role); + const now = Date.now(); + this.sweepTypingPresence(now); + const orgPrefix = `${organizationId}|`; + const byRoom = new Map(); + for (const [k, at] of this.typingPresence) { + if (!k.startsWith(orgPrefix)) continue; + if (now - at > 4000) { + this.typingPresence.delete(k); + continue; + } + const rest = k.slice(orgPrefix.length); + const sep = rest.lastIndexOf('|'); + if (sep <= 0) continue; + const roomId = rest.slice(0, sep); + const uid = rest.slice(sep + 1); + if (!roomId || !uid || uid === userId) continue; + const list = byRoom.get(roomId) || []; + list.push(uid); + byRoom.set(roomId, list); + } + if (!byRoom.size) return []; + const allIds = [...new Set([...byRoom.values()].flat())]; + const users = await this.prisma.user.findMany({ + where: { id: { in: allIds } }, + select: { id: true, fullName: true }, + }); + const nameById = new Map(users.map((u) => [u.id, u.fullName || 'کاربر'])); + const out: Array<{ + externalRoomId: string; + userId: string; + fullName: string; + chatUserId: string; + }> = []; + for (const [roomId, ids] of byRoom) { + for (const uid of ids) { + out.push({ + externalRoomId: roomId, + userId: uid, + fullName: nameById.get(uid) || 'کاربر', + chatUserId: buildChatUserId(organizationId, uid), + }); + } + } + return out; + } + async getAccess(userId: string, organizationId: string, role: string): Promise { const access = await this.loadAccess(userId, organizationId, role); return access; diff --git a/apps/web/src/lib/useOrgChat.ts b/apps/web/src/lib/useOrgChat.ts index 059336c..9d7e1ce 100644 --- a/apps/web/src/lib/useOrgChat.ts +++ b/apps/web/src/lib/useOrgChat.ts @@ -267,16 +267,8 @@ function applyTypingFromServer( at: now, })) .filter((t) => t.userId && t.userId !== sessionCache?.chatUserId); - const prev = typingMap.value[rid] || []; - // نگه داشتن رویدادهای خیلی تازه از WS اگر سرور هنوز خالی است - const merged = new Map(); - for (const t of prev) { - if (now - t.at < 4000) merged.set(t.userId, t); - } - for (const t of list) merged.set(t.userId, t); - const nextList = [...merged.values()]; - if (!nextList.length) { - if (prev.length) { + if (!list.length) { + if (typingMap.value[rid]) { const next = { ...typingMap.value }; delete next[rid]; typingMap.value = next; @@ -284,7 +276,31 @@ function applyTypingFromServer( return; } startTypingSweep(); - typingMap.value = { ...typingMap.value, [rid]: nextList }; + typingMap.value = { ...typingMap.value, [rid]: list }; +} + +function applyTypingFromServerAll( + rows: Array<{ + externalRoomId: string; + userId: string; + fullName: string; + chatUserId: string; + }>, +) { + const now = Date.now(); + const next: Record> = {}; + for (const row of rows) { + const rid = String(row.externalRoomId || '').trim(); + const uid = String(row.chatUserId || row.userId || '').trim(); + if (!rid || !uid || uid === sessionCache?.chatUserId) continue; + const list = next[rid] || []; + if (!list.some((t) => t.userId === uid)) { + list.push({ userId: uid, name: row.fullName || undefined, at: now }); + } + next[rid] = list; + } + typingMap.value = next; + if (Object.keys(next).length) startTypingSweep(); } function parseTypingCommand(content: string): string | null { @@ -318,17 +334,21 @@ function stopTypingPoll() { } } -function startTypingPoll(roomId: string) { - stopTypingPoll(); - const rid = String(roomId || '').trim(); - if (!rid) return; +/** poll همه اتاق‌ها تا پیش‌نمایش لیست هم «در حال نوشتن» بگیرد */ +function startTypingPoll() { + if (typingPollTimer) return; const tick = async () => { - if (activeRoomId.value !== rid) return; + if (!pageOpen.value) return; try { const { data } = await api.get< - Array<{ userId: string; fullName: string; chatUserId: string }> - >('/conversations/rooms/typing', { params: { externalRoomId: rid } }); - applyTypingFromServer(rid, Array.isArray(data) ? data : []); + Array<{ + externalRoomId: string; + userId: string; + fullName: string; + chatUserId: string; + }> + >('/conversations/rooms/typing'); + applyTypingFromServerAll(Array.isArray(data) ? data : []); } catch { // ignore } @@ -1130,6 +1150,11 @@ export function useOrgChat() { function setConversationsPageOpen(open: boolean) { pageOpen.value = open; + if (open) startTypingPoll(); + else { + stopTypingPoll(); + typingMap.value = {}; + } } function normalizeMessageList( @@ -1164,9 +1189,8 @@ export function useOrgChat() { loadingOlderMessages.value = false; fetchedRawCount = 0; hasMoreMessages.value = false; - clearTypingForRoom(roomId); startHiddenPoll(roomId); - startTypingPoll(roomId); + startTypingPoll(); try { const session = await ensureSession(); await connectSocket(); @@ -1637,7 +1661,6 @@ export function useOrgChat() { function clearActiveRoom() { stopHiddenPoll(); - stopTypingPoll(); activeRoomId.value = ''; messages.value = []; } diff --git a/apps/web/src/views/ConversationsView.vue b/apps/web/src/views/ConversationsView.vue index 25f9c5c..bb2b6e6 100644 --- a/apps/web/src/views/ConversationsView.vue +++ b/apps/web/src/views/ConversationsView.vue @@ -341,7 +341,31 @@ function deliveryLabel(delivery?: ChatDelivery) { return '…'; } +function roomTypingPreview(roomId: string): string { + void typingTick.value; + const list = (typingMap.value?.[roomId] || []).filter( + (t) => Date.now() - t.at < 4000, + ); + if (!list.length) return ''; + const names = list + .map((t) => { + if (t.name) return t.name; + if (t.userId === '__someone__') return ''; + return ( + peers.value.find((p) => p.chatUserId === t.userId)?.fullName || + peers.value.find((p) => p.id === t.userId)?.fullName || + '' + ); + }) + .filter(Boolean); + if (!names.length) return 'در حال نوشتن…'; + if (names.length === 1) return `${names[0]} در حال نوشتن…`; + return `${toFaDigits(names.length)} نفر در حال نوشتن…`; +} + function roomPreviewText(room: ConversationRoomDto) { + const typing = roomTypingPreview(room.externalRoomId); + if (typing) return typing; const raw = (room.lastMessagePreview || '').trim(); if (!raw) return 'بدون پیام'; if (raw === 'وویس') return 'پیام صوتی'; @@ -1648,7 +1672,9 @@ onUnmounted(() => { {{ roomTitle(room) }} - {{ roomPreviewText(room) }} + {{ + roomPreviewText(room) + }} {{ toFaDigits(room.unreadCount) }} @@ -2694,6 +2720,10 @@ onUnmounted(() => { max-width: 100%; display: block; } +.room-meta small.room-typing { + color: #0f766e; + font-weight: 600; +} .unread { grid-column: 3; grid-row: 1;