Convert video to file and change the appearance of mobile chat
This commit is contained in:
@@ -89,11 +89,40 @@ function normalizeMediaType(raw: string): ChatMediaType {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
const VIDEO_EXT_RE = /\.(mp4|webm|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;
|
||||
|
||||
function looksLikeVideo(pathOrName: string): boolean {
|
||||
return VIDEO_EXT_RE.test(pathOrName);
|
||||
}
|
||||
|
||||
function inferMediaType(
|
||||
declared: ChatMediaType,
|
||||
content: string,
|
||||
fileName?: string,
|
||||
): ChatMediaType {
|
||||
const probe = `${fileName || ''} ${content}`;
|
||||
if (declared === 'video' || looksLikeVideo(probe)) return 'video';
|
||||
if (declared === 'image') return 'image';
|
||||
if (declared === 'voice') return 'voice';
|
||||
if (declared === 'file' || declared === 'text') {
|
||||
if (looksLikeVideo(probe)) return 'video';
|
||||
if (IMAGE_EXT_RE.test(probe)) return 'image';
|
||||
if (AUDIO_EXT_RE.test(probe)) return 'voice';
|
||||
if (declared === 'file') return 'file';
|
||||
}
|
||||
return declared;
|
||||
}
|
||||
|
||||
export function previewLabelForMessage(msg: Pick<ChatMessage, 'type' | 'content' | 'fileName'>) {
|
||||
if (msg.type === 'image') return 'تصویر';
|
||||
if (msg.type === 'voice') return 'وویس';
|
||||
if (msg.type === 'voice') return 'پیام صوتی';
|
||||
if (msg.type === 'video') return 'ویدیو';
|
||||
if (msg.type === 'file') return msg.fileName?.trim() || 'فایل';
|
||||
if (msg.type === 'file') {
|
||||
if (looksLikeVideo(`${msg.fileName || ''} ${msg.content}`)) return 'ویدیو';
|
||||
return msg.fileName?.trim() || 'فایل';
|
||||
}
|
||||
return msg.content || 'پیام';
|
||||
}
|
||||
|
||||
@@ -155,12 +184,12 @@ function classifyFile(file: File): Exclude<ChatMediaType, 'text'> {
|
||||
return 'file';
|
||||
}
|
||||
|
||||
/** WS only officially documents image|voice; map video/file to closest supported + file. */
|
||||
function wsMediaType(type: ChatMediaType): 'image' | 'voice' | 'video' | 'file' {
|
||||
/** Chat backend (Hamkar-compatible) accepts text|image|voice|file — not video. */
|
||||
function wsMediaType(type: ChatMediaType): 'image' | 'voice' | 'file' | 'text' {
|
||||
if (type === 'image') return 'image';
|
||||
if (type === 'voice') return 'voice';
|
||||
if (type === 'video') return 'video';
|
||||
return 'file';
|
||||
if (type === 'video' || type === 'file') return 'file';
|
||||
return 'text';
|
||||
}
|
||||
|
||||
async function compressImageIfNeeded(file: File): Promise<File> {
|
||||
@@ -211,16 +240,20 @@ function normalizeIncoming(
|
||||
raw.message && typeof raw.message === 'object'
|
||||
? (raw.message as Record<string, unknown>)
|
||||
: raw;
|
||||
const type = normalizeMediaType(String(base.type_content ?? base.type ?? 'text'));
|
||||
const declared = normalizeMediaType(String(base.type_content ?? base.type ?? 'text'));
|
||||
const content = String(base.content ?? base.text ?? base.body ?? '').trim();
|
||||
if (!content && !base.attachments) return null;
|
||||
|
||||
const id = String(base.id ?? base.message_id ?? '').trim();
|
||||
const readAt = base.read_at ?? base.readAt ?? null;
|
||||
const downloadUrl = sessionCache?.downloadUrl ?? '';
|
||||
const fileName =
|
||||
String(base.file_name ?? base.fileName ?? base.name ?? '').trim() ||
|
||||
content.split('/').pop() ||
|
||||
undefined;
|
||||
const type = inferMediaType(declared, content, fileName);
|
||||
const isMedia = type !== 'text';
|
||||
const mediaUrl = isMedia ? buildChatFileUrl(content, downloadUrl) : undefined;
|
||||
const fileName = String(base.file_name ?? base.fileName ?? base.name ?? '').trim() || undefined;
|
||||
|
||||
return {
|
||||
id: id || `tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
@@ -250,8 +283,13 @@ function mergeMessages(incoming: ChatMessage, list: ChatMessage[]): ChatMessage[
|
||||
...prev,
|
||||
...incoming,
|
||||
optimistic: false,
|
||||
type:
|
||||
prev.type === 'video' && (incoming.type === 'file' || incoming.type === 'text')
|
||||
? 'video'
|
||||
: inferMediaType(incoming.type, incoming.content, incoming.fileName || prev.fileName),
|
||||
localPreviewUrl: incoming.localPreviewUrl ?? prev.localPreviewUrl,
|
||||
mediaUrl: incoming.mediaUrl || prev.mediaUrl,
|
||||
fileName: incoming.fileName || prev.fileName,
|
||||
delivery:
|
||||
incoming.delivery === 'read' || prev.delivery === 'read'
|
||||
? 'read'
|
||||
@@ -278,9 +316,14 @@ function mergeMessages(incoming: ChatMessage, list: ChatMessage[]): ChatMessage[
|
||||
...prev,
|
||||
...incoming,
|
||||
optimistic: false,
|
||||
localPreviewUrl: undefined,
|
||||
type:
|
||||
prev.type === 'video' && (incoming.type === 'file' || incoming.type === 'text')
|
||||
? 'video'
|
||||
: inferMediaType(incoming.type, incoming.content, incoming.fileName || prev.fileName),
|
||||
localPreviewUrl: prev.localPreviewUrl,
|
||||
uploadProgress: undefined,
|
||||
mediaUrl: incoming.mediaUrl || prev.mediaUrl,
|
||||
fileName: incoming.fileName || prev.fileName,
|
||||
delivery: incoming.readAt ? 'read' : 'sent',
|
||||
};
|
||||
if (prev.localPreviewUrl?.startsWith('blob:')) {
|
||||
@@ -477,6 +520,26 @@ function sendRead(roomId: string) {
|
||||
socket.send(JSON.stringify({ type: 'read', room_id: roomId }));
|
||||
}
|
||||
|
||||
function formatUploadFailure(status: number, bodyText: string): string {
|
||||
const raw = bodyText.trim();
|
||||
if (raw) {
|
||||
try {
|
||||
const json = JSON.parse(raw) as Record<string, unknown>;
|
||||
const err = String(json.error ?? json.message ?? '').trim();
|
||||
const details = String(json.details ?? '').trim();
|
||||
if (err && details) return `آپلود ناموفق بود: ${err} (${details})`;
|
||||
if (err) return `آپلود ناموفق بود: ${err}`;
|
||||
} catch {
|
||||
const snippet = raw.slice(0, 160);
|
||||
if (snippet) return `آپلود ناموفق بود (${status}): ${snippet}`;
|
||||
}
|
||||
}
|
||||
if (status === 500) {
|
||||
return 'آپلود ناموفق بود (۵۰۰). سرویس ذخیرهسازی فایل (S3) روی آدرس آپلود در دسترس نیست — تنظیمات upload/download در سوپرادمین را بررسی کنید.';
|
||||
}
|
||||
return `آپلود ناموفق بود (${status})`;
|
||||
}
|
||||
|
||||
function uploadChatFile(
|
||||
uploadUrl: string,
|
||||
file: File,
|
||||
@@ -497,7 +560,7 @@ function uploadChatFile(
|
||||
};
|
||||
xhr.onload = () => {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
reject(new Error(`آپلود ناموفق بود (${xhr.status})`));
|
||||
reject(new Error(formatUploadFailure(xhr.status, xhr.responseText || '')));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -649,6 +712,7 @@ export function useOrgChat() {
|
||||
content: filePath,
|
||||
};
|
||||
if (reply) payload.reply = reply;
|
||||
if (opts?.fileName) payload.file_name = opts.fileName;
|
||||
|
||||
const existingIdx = messages.value.findIndex((m) => m.id === id);
|
||||
if (existingIdx >= 0) {
|
||||
|
||||
Reference in New Issue
Block a user