Fixing the pop-up error and enabling the ability to reply to messages.
This commit is contained in:
@@ -11,6 +11,7 @@ import { formatClockFa, toFaDigits } from '@/lib/jalali';
|
||||
import {
|
||||
classifyChatFile,
|
||||
onRoomLiveEvent,
|
||||
previewLabelForMessage,
|
||||
useOrgChat,
|
||||
type ChatDelivery,
|
||||
type ChatMessage,
|
||||
@@ -49,7 +50,9 @@ const fileInputEl = ref<HTMLInputElement | null>(null);
|
||||
const draftInputEl = ref<HTMLTextAreaElement | null>(null);
|
||||
const emojiOpen = ref(false);
|
||||
const editingMessageId = ref<string | null>(null);
|
||||
const msgMenu = ref<{ id: string; x: number; y: number } | null>(null);
|
||||
const replyTo = ref<ChatMessage | null>(null);
|
||||
const msgMenu = ref<{ id: string; x: number; y: number; sheet: boolean } | null>(null);
|
||||
const msgMenuEl = ref<HTMLElement | null>(null);
|
||||
|
||||
const CHAT_EMOJIS = [
|
||||
'😀', '😁', '😂', '🤣', '😊', '😍', '😘', '😎', '🤔', '😅',
|
||||
@@ -137,6 +140,7 @@ function closeMobileChat() {
|
||||
activeRoom.value = null;
|
||||
clearPendingAttaches();
|
||||
cancelEdit();
|
||||
cancelReply();
|
||||
exitSelectMode();
|
||||
emojiOpen.value = false;
|
||||
msgMenu.value = null;
|
||||
@@ -261,9 +265,23 @@ function cancelEdit() {
|
||||
editingMessageId.value = null;
|
||||
}
|
||||
|
||||
function cancelReply() {
|
||||
replyTo.value = null;
|
||||
}
|
||||
|
||||
function startReplyMessage(msg: ChatMessage) {
|
||||
if (msg.deleted || msg.optimistic) return;
|
||||
replyTo.value = msg;
|
||||
editingMessageId.value = null;
|
||||
emojiOpen.value = false;
|
||||
msgMenu.value = null;
|
||||
nextTick(() => draftInputEl.value?.focus());
|
||||
}
|
||||
|
||||
function startEditMessage(msg: ChatMessage) {
|
||||
if (msg.deleted || msg.type !== 'text' || msg.optimistic) return;
|
||||
editingMessageId.value = msg.id;
|
||||
replyTo.value = null;
|
||||
draft.value = msg.content;
|
||||
emojiOpen.value = false;
|
||||
msgMenu.value = null;
|
||||
@@ -271,6 +289,26 @@ function startEditMessage(msg: ChatMessage) {
|
||||
nextTick(() => draftInputEl.value?.focus());
|
||||
}
|
||||
|
||||
function replyPreviewText(replyId: string | null | undefined) {
|
||||
if (!replyId) return 'پیام';
|
||||
const m = messages.value.find((item) => item.id === replyId);
|
||||
if (!m) return 'پیام';
|
||||
if (m.type === 'text') {
|
||||
const t = (m.content || '').trim();
|
||||
return t.length > 90 ? `${t.slice(0, 90)}…` : t || 'پیام';
|
||||
}
|
||||
return previewLabelForMessage(m);
|
||||
}
|
||||
|
||||
function scrollToMessage(id: string | null | undefined) {
|
||||
if (!id || !msgStreamEl.value) return;
|
||||
const row = msgStreamEl.value.querySelector(`[data-msg-id="${CSS.escape(id)}"]`);
|
||||
if (!(row instanceof HTMLElement)) return;
|
||||
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
row.classList.add('flash');
|
||||
window.setTimeout(() => row.classList.remove('flash'), 1200);
|
||||
}
|
||||
|
||||
async function confirmDeleteMessage(msg: ChatMessage) {
|
||||
msgMenu.value = null;
|
||||
if (!activeRoom.value || msg.optimistic || msg.deleted) return;
|
||||
@@ -336,10 +374,23 @@ function openMsgMenu(msg: ChatMessage, event: MouseEvent | TouchEvent) {
|
||||
x = (event as MouseEvent).clientX;
|
||||
y = (event as MouseEvent).clientY;
|
||||
}
|
||||
const pad = 12;
|
||||
x = Math.min(Math.max(x, pad + 80), window.innerWidth - pad - 80);
|
||||
y = Math.min(Math.max(y, pad + 40), window.innerHeight - pad);
|
||||
msgMenu.value = { id: msg.id, x, y };
|
||||
const sheet = window.matchMedia('(max-width: 900px)').matches;
|
||||
msgMenu.value = { id: msg.id, x, y, sheet };
|
||||
if (sheet) return;
|
||||
nextTick(() => {
|
||||
const el = msgMenuEl.value;
|
||||
if (!el || !msgMenu.value) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const pad = 12;
|
||||
let left = x - rect.width / 2;
|
||||
let top = y - rect.height - 10;
|
||||
if (top < pad) top = y + 10;
|
||||
if (top + rect.height > window.innerHeight - pad) {
|
||||
top = Math.max(pad, window.innerHeight - pad - rect.height);
|
||||
}
|
||||
left = Math.min(Math.max(left, pad), window.innerWidth - pad - rect.width);
|
||||
msgMenu.value = { ...msgMenu.value, x: left, y: top };
|
||||
});
|
||||
}
|
||||
|
||||
function menuMessage(): ChatMessage | null {
|
||||
@@ -627,17 +678,19 @@ async function submitMessage() {
|
||||
sending.value = true;
|
||||
error.value = '';
|
||||
const roomId = activeRoom.value.externalRoomId;
|
||||
const replyId = replyTo.value?.id ?? null;
|
||||
draft.value = '';
|
||||
pendingAttaches.value = [];
|
||||
emojiOpen.value = false;
|
||||
replyTo.value = null;
|
||||
|
||||
try {
|
||||
if (text) {
|
||||
await sendText(roomId, text);
|
||||
await sendText(roomId, text, replyId);
|
||||
}
|
||||
for (const item of attaches) {
|
||||
try {
|
||||
await sendFile(roomId, item.file);
|
||||
await sendFile(roomId, item.file, replyId);
|
||||
} finally {
|
||||
if (item.previewUrl.startsWith('blob:')) {
|
||||
try {
|
||||
@@ -704,6 +757,7 @@ watch(activeRoom, () => {
|
||||
clearPendingAttaches();
|
||||
exitSelectMode();
|
||||
cancelEdit();
|
||||
cancelReply();
|
||||
emojiOpen.value = false;
|
||||
msgMenu.value = null;
|
||||
showForward.value = false;
|
||||
@@ -859,6 +913,7 @@ onUnmounted(() => {
|
||||
v-for="msg in messages"
|
||||
:key="msg.id"
|
||||
class="msg-row"
|
||||
:data-msg-id="msg.id"
|
||||
:class="{
|
||||
mine: msg.senderId === chatUserId || msg.optimistic,
|
||||
selected: isSelected(msg.id),
|
||||
@@ -886,6 +941,15 @@ onUnmounted(() => {
|
||||
@touchmove="onBubbleTouchEnd"
|
||||
@touchcancel="onBubbleTouchEnd"
|
||||
>
|
||||
<button
|
||||
v-if="msg.reply"
|
||||
type="button"
|
||||
class="reply-quote"
|
||||
@click.stop="scrollToMessage(msg.reply)"
|
||||
>
|
||||
<span class="reply-quote-label">پاسخ به</span>
|
||||
<span class="reply-quote-text">{{ replyPreviewText(msg.reply) }}</span>
|
||||
</button>
|
||||
<template v-if="msg.type === 'image'">
|
||||
<button
|
||||
type="button"
|
||||
@@ -981,6 +1045,14 @@ onUnmounted(() => {
|
||||
<span>ویرایش پیام</span>
|
||||
<button type="button" class="edit-cancel" @click="cancelEdit(); draft = ''">انصراف</button>
|
||||
</div>
|
||||
<div v-else-if="replyTo" class="edit-banner reply-banner">
|
||||
<UiIcon name="chat" :size="16" />
|
||||
<div class="reply-banner-body">
|
||||
<strong>پاسخ به پیام</strong>
|
||||
<span>{{ replyPreviewText(replyTo.id) }}</span>
|
||||
</div>
|
||||
<button type="button" class="edit-cancel" @click="cancelReply">انصراف</button>
|
||||
</div>
|
||||
<div v-if="pendingAttaches.length" class="attach-preview">
|
||||
<div v-for="item in pendingAttaches" :key="item.id" class="attach-chip" :class="item.kind">
|
||||
<img v-if="item.kind === 'image'" :src="item.previewUrl" :alt="item.name" />
|
||||
@@ -1063,58 +1135,64 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
v-if="msgMenu"
|
||||
class="msg-menu-backdrop"
|
||||
@click="msgMenu = null"
|
||||
@touchstart.passive="msgMenu = null"
|
||||
/>
|
||||
<div
|
||||
v-if="msgMenu && menuMessage()"
|
||||
class="msg-menu"
|
||||
:style="{ top: `${msgMenu.y}px`, left: `${msgMenu.x}px` }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="enterSelectMode(menuMessage()!)"
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="msgMenu"
|
||||
class="msg-menu-backdrop"
|
||||
@click="msgMenu = null"
|
||||
@touchstart.passive="msgMenu = null"
|
||||
/>
|
||||
<div
|
||||
v-if="msgMenu && menuMessage()"
|
||||
ref="msgMenuEl"
|
||||
class="msg-menu"
|
||||
:class="{ sheet: msgMenu.sheet }"
|
||||
:style="
|
||||
msgMenu.sheet
|
||||
? undefined
|
||||
: { top: `${msgMenu.y}px`, left: `${msgMenu.x}px` }
|
||||
"
|
||||
>
|
||||
<UiIcon name="check-plain" :size="16" />
|
||||
انتخاب
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="openForwardPicker([menuMessage()!])"
|
||||
>
|
||||
<UiIcon name="send" :size="16" />
|
||||
ارسال برای دیگری
|
||||
</button>
|
||||
<button
|
||||
v-if="isMineMessage(menuMessage()) && menuMessage()?.type === 'text'"
|
||||
type="button"
|
||||
@click="startEditMessage(menuMessage()!)"
|
||||
>
|
||||
<UiIcon name="edit" :size="16" />
|
||||
ویرایش
|
||||
</button>
|
||||
<button
|
||||
v-if="isMineMessage(menuMessage())"
|
||||
type="button"
|
||||
class="danger"
|
||||
@click="confirmDeleteMessage(menuMessage()!)"
|
||||
>
|
||||
<UiIcon name="trash" :size="16" />
|
||||
حذف برای همه
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="danger"
|
||||
@click="confirmDeleteMessage(menuMessage()!)"
|
||||
>
|
||||
<UiIcon name="trash" :size="16" />
|
||||
حذف فقط برای من
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" @click="startReplyMessage(menuMessage()!)">
|
||||
<UiIcon name="chat" :size="16" />
|
||||
پاسخ
|
||||
</button>
|
||||
<button type="button" @click="enterSelectMode(menuMessage()!)">
|
||||
<UiIcon name="check-plain" :size="16" />
|
||||
انتخاب
|
||||
</button>
|
||||
<button type="button" @click="openForwardPicker([menuMessage()!])">
|
||||
<UiIcon name="send" :size="16" />
|
||||
ارسال برای دیگری
|
||||
</button>
|
||||
<button
|
||||
v-if="isMineMessage(menuMessage()) && menuMessage()?.type === 'text'"
|
||||
type="button"
|
||||
@click="startEditMessage(menuMessage()!)"
|
||||
>
|
||||
<UiIcon name="edit" :size="16" />
|
||||
ویرایش
|
||||
</button>
|
||||
<button
|
||||
v-if="isMineMessage(menuMessage())"
|
||||
type="button"
|
||||
class="danger"
|
||||
@click="confirmDeleteMessage(menuMessage()!)"
|
||||
>
|
||||
<UiIcon name="trash" :size="16" />
|
||||
حذف برای همه
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="danger"
|
||||
@click="confirmDeleteMessage(menuMessage()!)"
|
||||
>
|
||||
<UiIcon name="trash" :size="16" />
|
||||
حذف فقط برای من
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1793,14 +1871,14 @@ onUnmounted(() => {
|
||||
.msg-menu-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
background: transparent;
|
||||
z-index: 1200;
|
||||
background: rgba(15, 23, 42, 0.28);
|
||||
}
|
||||
.msg-menu {
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
transform: translate(-50%, -110%);
|
||||
min-width: 10rem;
|
||||
z-index: 1210;
|
||||
min-width: 11rem;
|
||||
max-width: min(18rem, calc(100vw - 1.5rem));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
@@ -1808,7 +1886,18 @@ onUnmounted(() => {
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.16);
|
||||
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
.msg-menu.sheet {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
top: auto;
|
||||
bottom: calc(0.85rem + env(safe-area-inset-bottom, 0px));
|
||||
transform: translateX(-50%);
|
||||
width: min(22rem, calc(100vw - 1.5rem));
|
||||
max-width: none;
|
||||
padding: 0.45rem;
|
||||
border-radius: 18px;
|
||||
}
|
||||
.msg-menu button {
|
||||
display: flex;
|
||||
@@ -1816,7 +1905,7 @@ onUnmounted(() => {
|
||||
gap: 0.45rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0.55rem 0.7rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-radius: 10px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
@@ -1830,6 +1919,57 @@ onUnmounted(() => {
|
||||
.msg-menu button.danger {
|
||||
color: #dc2626;
|
||||
}
|
||||
.reply-quote {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.15rem;
|
||||
width: 100%;
|
||||
margin: 0 0 0.4rem;
|
||||
padding: 0.4rem 0.55rem;
|
||||
border: none;
|
||||
border-inline-start: 3px solid #0d9488;
|
||||
border-radius: 10px;
|
||||
background: rgba(15, 23, 42, 0.05);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
}
|
||||
.reply-quote-label {
|
||||
font-size: 0.72rem;
|
||||
color: #0f766e;
|
||||
font-weight: 700;
|
||||
}
|
||||
.reply-quote-text {
|
||||
font-size: 0.82rem;
|
||||
color: #475569;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
.reply-banner-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.reply-banner-body strong {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.reply-banner-body span {
|
||||
font-size: 0.78rem;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.msg-row.flash .bubble {
|
||||
outline: 2px solid rgba(13, 148, 136, 0.55);
|
||||
transition: outline-color 0.2s ease;
|
||||
}
|
||||
.attach-preview {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
|
||||
Reference in New Issue
Block a user