Convert video to file and change the appearance of mobile chat
This commit is contained in:
@@ -446,11 +446,13 @@ export class ConversationsService {
|
||||
if (lastType === 'image' || lastType === 'photo' || lastType === 'img') {
|
||||
preview = 'تصویر';
|
||||
} else if (lastType === 'voice' || lastType === 'audio') {
|
||||
preview = 'وویس';
|
||||
preview = 'پیام صوتی';
|
||||
} else if (lastType === 'video') {
|
||||
preview = 'ویدیو';
|
||||
} else if (lastType === 'file' || lastType === 'document' || lastType === 'attachment') {
|
||||
preview = 'فایل';
|
||||
preview = /\.(mp4|webm|mov|m4v|mkv|avi|3gp)(?:\?|$)/i.test(preview)
|
||||
? 'ویدیو'
|
||||
: 'فایل';
|
||||
}
|
||||
const lastAtRaw =
|
||||
lastMsg?.created_at ?? lastMsg?.createdAt ?? row.updated_at ?? row.updatedAt ?? null;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -104,6 +104,22 @@ function deliveryLabel(delivery?: ChatDelivery) {
|
||||
return '…';
|
||||
}
|
||||
|
||||
function roomPreviewText(room: ConversationRoomDto) {
|
||||
const raw = (room.lastMessagePreview || '').trim();
|
||||
if (!raw) return 'بدون پیام';
|
||||
if (raw === 'وویس') return 'پیام صوتی';
|
||||
return raw;
|
||||
}
|
||||
|
||||
function closeMobileChat() {
|
||||
clearActiveRoom();
|
||||
activeRoom.value = null;
|
||||
clearPendingAttaches();
|
||||
if (recording.value) mediaRecorder?.stop();
|
||||
}
|
||||
|
||||
const mobileChatOpen = computed(() => Boolean(activeRoom.value));
|
||||
|
||||
function classifyPending(file: File): PendingAttach['kind'] {
|
||||
const mime = (file.type || '').toLowerCase();
|
||||
if (mime.startsWith('image/')) return 'image';
|
||||
@@ -446,25 +462,30 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="conv-page">
|
||||
<div class="conv-page" :class="{ 'chat-open': mobileChatOpen }">
|
||||
<div class="conv-aurora" aria-hidden="true" />
|
||||
|
||||
<header class="conv-head">
|
||||
<header class="conv-head desktop-head">
|
||||
<div>
|
||||
<p class="eyebrow">درون سازمان</p>
|
||||
<h1>گفتگو</h1>
|
||||
<p class="sub">فضای زنده برای هماهنگی تیم — بدون پیامرسان بیرونی.</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<div class="head-actions">
|
||||
<span class="ws-pill" :data-state="status">{{ status === 'open' ? 'آنلاین' : '…' }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<header class="mobile-list-head" :hidden="mobileChatOpen">
|
||||
<h1>گفتگو</h1>
|
||||
<span class="ws-pill" :data-state="status">{{ status === 'open' ? 'آنلاین' : '…' }}</span>
|
||||
</header>
|
||||
|
||||
<p v-if="error" class="err-banner">{{ error }}</p>
|
||||
<p v-else-if="loading" class="muted center">در حال آمادهسازی…</p>
|
||||
|
||||
<div v-else class="conv-shell">
|
||||
<aside class="conv-rail">
|
||||
<aside class="conv-rail" :hidden="mobileChatOpen">
|
||||
<div class="rail-actions">
|
||||
<button type="button" class="fab" @click="showNewDm = true">+ گفتگو</button>
|
||||
<button
|
||||
@@ -489,7 +510,7 @@ onUnmounted(() => {
|
||||
</span>
|
||||
<span class="room-meta">
|
||||
<strong>{{ roomTitle(room) }}</strong>
|
||||
<small>{{ room.lastMessagePreview || 'بدون پیام' }}</small>
|
||||
<small>{{ roomPreviewText(room) }}</small>
|
||||
</span>
|
||||
<span v-if="room.unreadCount" class="unread">{{ toFaDigits(room.unreadCount) }}</span>
|
||||
<time>{{ formatListTime(room.lastMessageAt) }}</time>
|
||||
@@ -498,8 +519,11 @@ onUnmounted(() => {
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<section class="conv-stage">
|
||||
<div v-if="!activeRoom" class="empty-stage">
|
||||
<section
|
||||
class="conv-stage"
|
||||
:class="{ empty: !activeRoom, open: mobileChatOpen }"
|
||||
>
|
||||
<div v-if="!activeRoom" class="empty-stage desktop-empty">
|
||||
<div class="empty-glow" />
|
||||
<UiIcon name="chat" :size="48" />
|
||||
<h2>یک مکالمه را انتخاب کنید</h2>
|
||||
@@ -508,8 +532,16 @@ onUnmounted(() => {
|
||||
|
||||
<template v-else>
|
||||
<div class="stage-top">
|
||||
<h2>{{ roomTitle(activeRoom) }}</h2>
|
||||
<span class="kind-tag">{{ activeRoom.kind === 'group' ? 'گروه' : 'خصوصی' }}</span>
|
||||
<button type="button" class="back-btn" aria-label="بازگشت" @click="closeMobileChat">
|
||||
<UiIcon name="arrow-right" :size="20" />
|
||||
</button>
|
||||
<span class="stage-avatar">
|
||||
<UserAvatar :name="roomTitle(activeRoom)" :size="36" />
|
||||
</span>
|
||||
<div class="stage-titles">
|
||||
<h2>{{ roomTitle(activeRoom) }}</h2>
|
||||
<span class="kind-tag">{{ activeRoom.kind === 'group' ? 'گروه' : 'خصوصی' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingMessages" class="muted center pad">در حال دریافت پیامها…</div>
|
||||
@@ -541,7 +573,24 @@ onUnmounted(() => {
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="msg.type === 'video'">
|
||||
<video class="media-video" :src="mediaSrc(msg)" controls preload="metadata" />
|
||||
<video
|
||||
v-if="mediaSrc(msg)"
|
||||
class="media-video"
|
||||
:src="mediaSrc(msg)"
|
||||
controls
|
||||
playsinline
|
||||
preload="metadata"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
class="media-file"
|
||||
:href="msg.content"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<UiIcon name="document" :size="18" />
|
||||
<span>{{ msg.fileName || 'ویدیو' }}</span>
|
||||
</a>
|
||||
<span v-if="msg.uploadProgress != null && msg.uploadProgress < 100" class="upload-line">
|
||||
آپلود {{ toFaDigits(msg.uploadProgress) }}٪
|
||||
</span>
|
||||
@@ -622,7 +671,7 @@ onUnmounted(() => {
|
||||
type="button"
|
||||
class="tool-btn"
|
||||
:class="{ on: recording }"
|
||||
title="ضبط وویس"
|
||||
title="ضبط پیام صوتی"
|
||||
:disabled="sending"
|
||||
@click="toggleVoiceRecord"
|
||||
>
|
||||
@@ -631,12 +680,14 @@ onUnmounted(() => {
|
||||
<input
|
||||
v-model="draft"
|
||||
type="text"
|
||||
placeholder="پیام خود را بنویسید…"
|
||||
class="composer-input"
|
||||
placeholder="پیام…"
|
||||
autocomplete="off"
|
||||
:disabled="sending || recording"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="send-btn"
|
||||
:disabled="sending || recording || (!draft.trim() && !pendingAttaches.length)"
|
||||
>
|
||||
<UiIcon name="send" :size="18" />
|
||||
@@ -718,6 +769,9 @@ onUnmounted(() => {
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.mobile-list-head {
|
||||
display: none;
|
||||
}
|
||||
.eyebrow {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
@@ -754,16 +808,6 @@ onUnmounted(() => {
|
||||
.ws-pill[data-state='open'] {
|
||||
box-shadow: 0 0 0 1px rgba(45, 212, 191, 0.35);
|
||||
}
|
||||
.gear {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
.err-banner {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
@@ -778,11 +822,7 @@ onUnmounted(() => {
|
||||
grid-template-columns: minmax(260px, 320px) 1fr;
|
||||
gap: 0.75rem;
|
||||
min-height: 520px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.conv-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
height: min(72vh, 720px);
|
||||
}
|
||||
.conv-rail {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
@@ -793,6 +833,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-height: 0;
|
||||
}
|
||||
.rail-actions {
|
||||
display: flex;
|
||||
@@ -818,12 +859,13 @@ onUnmounted(() => {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
max-height: calc(100vh - 14rem);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.room-chip {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 0.15rem 0.65rem;
|
||||
align-items: center;
|
||||
@@ -845,6 +887,9 @@ onUnmounted(() => {
|
||||
padding: 2px;
|
||||
background: linear-gradient(135deg, #2dd4bf, #a78bfa);
|
||||
}
|
||||
.room-meta {
|
||||
min-width: 0;
|
||||
}
|
||||
.room-meta strong {
|
||||
display: block;
|
||||
font-size: 0.92rem;
|
||||
@@ -855,7 +900,7 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 160px;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.unread {
|
||||
@@ -883,26 +928,55 @@ onUnmounted(() => {
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 480px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stage-top {
|
||||
padding: 1rem 1.15rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.55rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.back-btn {
|
||||
display: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 999px;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stage-avatar {
|
||||
flex-shrink: 0;
|
||||
border-radius: 999px;
|
||||
padding: 2px;
|
||||
background: linear-gradient(135deg, #2dd4bf, #a78bfa);
|
||||
}
|
||||
.stage-titles {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
.stage-top h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.05rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.kind-tag {
|
||||
font-size: 0.72rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(124, 58, 237, 0.1);
|
||||
color: #6d28d9;
|
||||
width: fit-content;
|
||||
}
|
||||
.msg-stream {
|
||||
flex: 1;
|
||||
@@ -913,6 +987,10 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
min-height: 0;
|
||||
background:
|
||||
radial-gradient(circle at 12% 8%, rgba(45, 212, 191, 0.07), transparent 40%),
|
||||
radial-gradient(circle at 88% 92%, rgba(167, 139, 250, 0.08), transparent 42%);
|
||||
}
|
||||
.msg-row {
|
||||
display: flex;
|
||||
@@ -1031,57 +1109,58 @@ onUnmounted(() => {
|
||||
.composer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem 1rem;
|
||||
gap: 0.45rem;
|
||||
padding: 0.65rem 0.75rem calc(0.65rem + env(safe-area-inset-bottom, 0px));
|
||||
border-top: 1px solid var(--line);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.composer-row {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.composer-row input[type='text'] {
|
||||
flex: 1;
|
||||
.composer-input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 0.65rem 0.85rem;
|
||||
font: inherit;
|
||||
background: #fff;
|
||||
}
|
||||
.composer-row > button[type='submit'] {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
background: #0d9488;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
.tool-btn,
|
||||
.send-btn {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 12px;
|
||||
width: 2.6rem;
|
||||
height: 2.6rem;
|
||||
min-width: 2.6rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.composer-row > button[type='submit']:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-btn {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface);
|
||||
background: var(--surface, #fff);
|
||||
color: var(--text);
|
||||
border-radius: 12px;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-btn.on {
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
.tool-btn:disabled {
|
||||
.tool-btn:disabled,
|
||||
.send-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.send-btn {
|
||||
border: none;
|
||||
background: #0d9488;
|
||||
color: #fff;
|
||||
}
|
||||
.attach-preview {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
@@ -1171,6 +1250,23 @@ onUnmounted(() => {
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
.empty-stage {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
gap: 0.35rem;
|
||||
color: var(--muted);
|
||||
position: relative;
|
||||
}
|
||||
.empty-glow {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(167, 139, 250, 0.25), transparent 70%);
|
||||
filter: blur(8px);
|
||||
}
|
||||
.lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -1201,23 +1297,6 @@ onUnmounted(() => {
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.empty-stage {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
gap: 0.35rem;
|
||||
color: var(--muted);
|
||||
position: relative;
|
||||
}
|
||||
.empty-glow {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(167, 139, 250, 0.25), transparent 70%);
|
||||
filter: blur(8px);
|
||||
}
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -1286,4 +1365,129 @@ onUnmounted(() => {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.conv-page {
|
||||
margin: -1rem -0.85rem calc(-1.25rem - var(--mobile-tabbar-h, 3.65rem));
|
||||
padding: 0;
|
||||
min-height: calc(100dvh - 3.5rem - var(--mobile-tabbar-h, 3.65rem));
|
||||
height: calc(100dvh - 3.5rem - var(--mobile-tabbar-h, 3.65rem));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.conv-page.chat-open {
|
||||
background: #fff;
|
||||
}
|
||||
.conv-aurora,
|
||||
.desktop-head {
|
||||
display: none;
|
||||
}
|
||||
.mobile-list-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mobile-list-head[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.mobile-list-head h1 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
color: #0f766e;
|
||||
}
|
||||
.err-banner {
|
||||
margin: 0.5rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.conv-shell {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
.conv-rail {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: #fff;
|
||||
backdrop-filter: none;
|
||||
padding: 0.5rem 0.35rem 0.75rem;
|
||||
}
|
||||
.conv-rail[hidden],
|
||||
.conv-page.chat-open .conv-rail {
|
||||
display: none !important;
|
||||
}
|
||||
.rail-actions {
|
||||
padding: 0 0.5rem 0.35rem;
|
||||
}
|
||||
.room-list {
|
||||
max-height: none;
|
||||
}
|
||||
.room-chip {
|
||||
border-radius: 0;
|
||||
padding: 0.7rem 0.75rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.room-chip.active,
|
||||
.room-chip:hover {
|
||||
background: rgba(13, 148, 136, 0.06);
|
||||
}
|
||||
.conv-stage {
|
||||
display: none;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: #fff;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.conv-stage.open {
|
||||
display: flex;
|
||||
}
|
||||
.desktop-empty {
|
||||
display: none;
|
||||
}
|
||||
.back-btn {
|
||||
display: grid;
|
||||
}
|
||||
.stage-top {
|
||||
padding: 0.55rem 0.65rem;
|
||||
background: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
.msg-stream {
|
||||
padding: 0.75rem 0.7rem 0.5rem;
|
||||
}
|
||||
.bubble {
|
||||
max-width: min(300px, 82%);
|
||||
}
|
||||
.composer {
|
||||
padding: 0.5rem 0.55rem calc(0.45rem + env(safe-area-inset-bottom, 0px));
|
||||
box-shadow: 0 -6px 18px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
.tool-btn,
|
||||
.send-btn {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
min-width: 2.75rem;
|
||||
}
|
||||
.composer-input {
|
||||
padding: 0.7rem 0.8rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user