From e20b904bbe915bda83725327d2a0fb771269f3d6 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Tue, 29 Sep 2026 20:48:14 +0330 Subject: [PATCH] Fixing video retrieval error --- .../conversations/conversations.service.ts | 8 +- apps/web/src/lib/useOrgChat.ts | 77 +++++++++++++------ 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/apps/api/src/conversations/conversations.service.ts b/apps/api/src/conversations/conversations.service.ts index 0236c5f..5d1cc16 100644 --- a/apps/api/src/conversations/conversations.service.ts +++ b/apps/api/src/conversations/conversations.service.ts @@ -444,13 +444,17 @@ export class ConversationsService { ? String(lastMsg.content ?? lastMsg.text ?? lastMsg.body ?? '').trim() : ''; if (lastType === 'image' || lastType === 'photo' || lastType === 'img') { - preview = 'تصویر'; + if (/(?:^|[\\/._-])vid-\d+/i.test(preview) || /\.(mp4|mov|m4v|mkv|avi|3gp)(?:\?|$)/i.test(preview)) { + preview = 'ویدیو'; + } else { + preview = 'تصویر'; + } } else if (lastType === 'voice' || lastType === 'audio') { preview = 'پیام صوتی'; } else if (lastType === 'video') { preview = 'ویدیو'; } else if (lastType === 'file' || lastType === 'document' || lastType === 'attachment') { - if (/\.(mp4|mov|m4v|mkv|avi|3gp)(?:\?|$)/i.test(preview)) { + if (/(?:^|[\\/._-])vid-\d+/i.test(preview) || /\.(mp4|mov|m4v|mkv|avi|3gp)(?:\?|$)/i.test(preview)) { preview = 'ویدیو'; } else if ( /(?:^|[\\/._-])voice(?:[\\/._-]|$)/i.test(preview) || diff --git a/apps/web/src/lib/useOrgChat.ts b/apps/web/src/lib/useOrgChat.ts index 5452737..9ec1c40 100644 --- a/apps/web/src/lib/useOrgChat.ts +++ b/apps/web/src/lib/useOrgChat.ts @@ -93,6 +93,8 @@ const VIDEO_EXT_RE = /\.(mp4|mov|m4v|mkv|avi|3gp|ogv)(?:\?|$)/i; const AUDIO_EXT_RE = /\.(webm|ogg|mp3|m4a|wav|aac|opus|oga)(?:\?|$)/i; const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp|bmp|svg|heic|heif)(?:\?|$)/i; const VOICE_NAME_RE = /(?:^|[\\/._-])voice(?:[\\/._-]|$)/i; +/** Uploaded chat videos are camouflaged as .jpg so the external WS accepts them as image. */ +const VIDEO_CAMO_RE = /(?:^|[\\/._-])vid-\d+/i; function looksLikeVideo(pathOrName: string): boolean { return VIDEO_EXT_RE.test(pathOrName); @@ -102,17 +104,24 @@ function looksLikeVoiceAsset(pathOrName: string): boolean { return VOICE_NAME_RE.test(pathOrName) || AUDIO_EXT_RE.test(pathOrName); } +function looksLikeCamouflagedVideo(pathOrName: string): boolean { + return VIDEO_CAMO_RE.test(pathOrName); +} + +function looksLikeVideoAsset(pathOrName: string): boolean { + return looksLikeVideo(pathOrName) || looksLikeCamouflagedVideo(pathOrName); +} + function inferMediaType( declared: ChatMediaType, content: string, fileName?: string, ): ChatMediaType { const probe = `${fileName || ''} ${content}`; - // Declared voice always wins — recordings are often .webm and must stay as audio UI - if (declared === 'voice') return 'voice'; - // Path/name cues beat wrong declared types (MediaRecorder video/webm, backend "video" on voice) - if (looksLikeVoiceAsset(probe) && !looksLikeVideo(probe)) return 'voice'; - if (looksLikeVideo(probe)) return 'video'; + // Voice recordings must stay audio UI (often .webm). Camouflaged videos never use voice names. + if (declared === 'voice' && !looksLikeVideoAsset(probe)) return 'voice'; + if (looksLikeVoiceAsset(probe) && !looksLikeVideoAsset(probe)) return 'voice'; + if (looksLikeVideoAsset(probe)) return 'video'; if (declared === 'video') return 'video'; if (declared === 'image') return 'image'; if (declared === 'file' || declared === 'text') { @@ -127,7 +136,7 @@ export function previewLabelForMessage(msg: Pick { } /** - * Chat backend accepts text|image|voice reliably. - * Real video is sent as `file` (path + .mp4 name) — `video` is dropped; `image` can also fail for large binaries. + * External chat WS reliably delivers text|image|voice. + * Native `video` is dropped; `file` with .mp4 is also dropped. + * Videos are uploaded as image-looking paths (vid-*.jpg) and sent as type image. */ function wsMediaType(type: ChatMediaType): 'image' | 'voice' | 'file' | 'text' { - if (type === 'image') return 'image'; - if (type === 'video') return 'file'; + if (type === 'image' || type === 'video') return 'image'; if (type === 'voice') return 'voice'; if (type === 'file') return 'file'; return 'text'; @@ -268,7 +277,16 @@ function normalizeIncoming( ? (raw.message as Record) : raw; const declared = normalizeMediaType(String(base.type_content ?? base.type ?? 'text')); - const content = String(base.content ?? base.text ?? base.body ?? '').trim(); + const content = String( + base.content ?? + base.text ?? + base.body ?? + base.file_url ?? + base.fileUrl ?? + base.url ?? + base.path ?? + '', + ).trim(); if (!content && !base.attachments) return null; const id = String(base.id ?? base.message_id ?? '').trim(); @@ -740,14 +758,20 @@ export function useOrgChat() { const id = opts?.tempId || crypto.randomUUID(); const reply = opts?.reply ?? null; const wsType = wsMediaType(type); + // Never put real video extensions in WS content — external chat drops those messages. let filePathHint = filePath; - if ( - (type === 'video' || type === 'voice') && - !/\.[a-z0-9]{2,5}(?:\?|$)/i.test(filePathHint) - ) { + if (type === 'voice' && !/\.[a-z0-9]{2,5}(?:\?|$)/i.test(filePathHint)) { const ext = opts?.fileName?.match(/(\.[a-z0-9]{2,5})$/i)?.[1]; if (ext) filePathHint = `${filePathHint}${ext}`; } + const displayName = + type === 'video' + ? opts?.fileName && looksLikeVideo(opts.fileName) + ? opts.fileName + : opts?.fileName + ? opts.fileName.replace(/\.[^.]+$/, '') + '.mp4' + : 'video.mp4' + : opts?.fileName; const mediaUrl = buildChatFileUrl(filePathHint, session.downloadUrl); const payload: Record = { @@ -757,7 +781,7 @@ export function useOrgChat() { content: filePathHint, }; if (reply) payload.reply = reply; - if (opts?.fileName) payload.file_name = opts.fileName; + if (displayName) payload.file_name = displayName; const existingIdx = messages.value.findIndex((m) => m.id === id); if (existingIdx >= 0) { @@ -767,7 +791,7 @@ export function useOrgChat() { content: filePathHint, type, mediaUrl, - fileName: opts?.fileName, + fileName: displayName, uploadProgress: 100, optimistic: false, delivery: 'sent', @@ -785,7 +809,7 @@ export function useOrgChat() { optimistic: true, delivery: 'sending', mediaUrl, - fileName: opts?.fileName, + fileName: displayName, localPreviewUrl: opts?.localPreviewUrl, }; messages.value = mergeMessages(optimistic, messages.value); @@ -796,7 +820,7 @@ export function useOrgChat() { preview: previewLabelForMessage({ type, content: filePathHint, - fileName: opts?.fileName, + fileName: displayName, }), at: new Date().toISOString(), fromSelf: true, @@ -857,10 +881,15 @@ export function useOrgChat() { try { setProgress(5); let fileForUpload = type === 'image' ? await compressImageIfNeeded(file) : file; - if (type === 'video' && !looksLikeVideo(fileForUpload.name)) { - const base = fileForUpload.name.replace(/\.[^.]+$/, '') || 'video'; - fileForUpload = new File([fileForUpload], `${base}.mp4`, { - type: fileForUpload.type || 'video/mp4', + let outboundFileName = file.name; + if (type === 'video') { + // Camouflage as image path so external chat WS delivers the message. + // Keep original name (with video ext) for UI inference via file_name. + const original = + looksLikeVideo(file.name) ? file.name : `${file.name.replace(/\.[^.]+$/, '') || 'video'}.mp4`; + outboundFileName = original; + fileForUpload = new File([file], `vid-${Date.now()}.jpg`, { + type: file.type || 'video/mp4', }); } setProgress(12); @@ -870,7 +899,7 @@ export function useOrgChat() { setProgress(99); await sendMediaPath(roomId, uploadedPath, type, { reply, - fileName: fileForUpload.name || file.name, + fileName: outboundFileName, localPreviewUrl, tempId: id, });