Convert video to file and change the appearance of mobile chat
This commit is contained in:
@@ -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