diff --git a/apps/web/src/lib/useOrgChat.ts b/apps/web/src/lib/useOrgChat.ts index 269946d..10fe92b 100644 --- a/apps/web/src/lib/useOrgChat.ts +++ b/apps/web/src/lib/useOrgChat.ts @@ -22,16 +22,33 @@ const messages = shallowRef([]); const activeRoomId = ref(''); let pingTimer: ReturnType | null = null; -function normalizeIncoming(raw: Record, roomFallback: string): ChatMessage | null { +function messageTs(item: ChatMessage): number { + const t = new Date(item.createdAt).getTime(); + return Number.isNaN(t) ? 0 : t; +} + +/** قدیمی → جدید (آخرین پیام پایین لیست) */ +function sortAsc(items: ChatMessage[]): ChatMessage[] { + return [...items].sort((a, b) => { + const d = messageTs(a) - messageTs(b); + if (d !== 0) return d; + return a.id.localeCompare(b.id); + }); +} + +function normalizeIncoming( + raw: Record, + roomFallback: string, +): ChatMessage | null { const base = raw.message && typeof raw.message === 'object' ? (raw.message as Record) : raw; const content = String(base.content ?? base.text ?? base.body ?? '').trim(); if (!content && !base.attachments) return null; - const id = String(base.id ?? base.message_id ?? crypto.randomUUID()); + const id = String(base.id ?? base.message_id ?? '').trim(); return { - id, + id: id || `tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, roomId: String(base.room_id ?? raw.room_id ?? roomFallback), senderId: String( base.sender_id ?? base.user_id ?? base.from_user_id ?? raw.sender_id ?? '', @@ -45,6 +62,36 @@ function normalizeIncoming(raw: Record, roomFallback: string): }; } +/** ادغام: همان id جایگزین می‌شود؛ optimistic مشابه با echo سوکت یکی می‌شود */ +function mergeMessages(incoming: ChatMessage, list: ChatMessage[]): ChatMessage[] { + const byId = list.findIndex((m) => m.id === incoming.id); + if (byId >= 0) { + const next = [...list]; + next[byId] = { ...list[byId], ...incoming, optimistic: false }; + return sortAsc(next); + } + + // echo سوکت بعد از optimistic با همان متن/فرستنده/اتاق + const optimisticIdx = list.findIndex( + (m) => + m.optimistic && + m.roomId === incoming.roomId && + m.senderId === incoming.senderId && + m.content === incoming.content, + ); + if (optimisticIdx >= 0) { + const next = [...list]; + next[optimisticIdx] = { + ...list[optimisticIdx], + ...incoming, + optimistic: false, + }; + return sortAsc(next); + } + + return sortAsc([...list, incoming]); +} + async function ensureSession(): Promise { if (sessionCache) return sessionCache; const { data } = await api.get('/conversations/session'); @@ -68,6 +115,21 @@ function disconnectSocket() { status.value = 'closed'; } +function onSocketMessage(event: MessageEvent) { + const raw = String(event.data ?? '').trim(); + if (!raw || raw === 'pong' || raw === 'ping') return; + try { + const parsed = JSON.parse(raw) as Record; + const type = String(parsed.type ?? '').toLowerCase(); + if (type === 'seen' || type === 'typing' || type === 'read' || type === 'pong') return; + const msg = normalizeIncoming(parsed, activeRoomId.value); + if (!msg || !activeRoomId.value || msg.roomId !== activeRoomId.value) return; + messages.value = mergeMessages(msg, messages.value); + } catch { + // ignore + } +} + async function connectSocket() { const session = await ensureSession(); if (socket?.readyState === WebSocket.OPEN) return; @@ -88,19 +150,7 @@ async function connectSocket() { clearPing(); }); - socket.addEventListener('message', (event) => { - const raw = String(event.data ?? '').trim(); - if (!raw || raw === 'pong' || raw === 'ping') return; - try { - const parsed = JSON.parse(raw) as Record; - if (parsed.type === 'seen' || parsed.type === 'typing' || parsed.type === 'read') return; - const msg = normalizeIncoming(parsed, activeRoomId.value); - if (!msg || msg.roomId !== activeRoomId.value) return; - messages.value = [...messages.value, msg]; - } catch { - // ignore - } - }); + socket.addEventListener('message', onSocketMessage); await new Promise((resolve, reject) => { if (!socket) return reject(new Error('socket missing')); @@ -145,9 +195,10 @@ export function useOrgChat() { if (!res.ok) throw new Error('messages failed'); const body = await res.json(); const list = Array.isArray(body) ? body : body?.data ?? []; - messages.value = list + const normalized = list .map((item: Record) => normalizeIncoming(item, roomId)) .filter(Boolean) as ChatMessage[]; + messages.value = sortAsc(normalized); } finally { loadingMessages.value = false; } @@ -177,7 +228,7 @@ export function useOrgChat() { reply, optimistic: true, }; - messages.value = [...messages.value, optimistic]; + messages.value = mergeMessages(optimistic, messages.value); socket.send(JSON.stringify(payload)); sendRead(roomId); } diff --git a/apps/web/src/views/ConversationsView.vue b/apps/web/src/views/ConversationsView.vue index 0d0761c..fe205e3 100644 --- a/apps/web/src/views/ConversationsView.vue +++ b/apps/web/src/views/ConversationsView.vue @@ -1,5 +1,5 @@