Fixed message deletion error and layout changes for the message input box.

This commit is contained in:
2026-09-30 09:19:36 +03:30
parent 58e89246d9
commit 298b3316b4
7 changed files with 176 additions and 76 deletions
+42 -24
View File
@@ -93,7 +93,11 @@ function isRememberedHidden(roomId: string, messageId: string): boolean {
return Boolean(hiddenIdsByRoom.get(roomId)?.has(messageId));
}
async function persistHiddenOnServer(externalRoomId: string, messageIds: string[]) {
async function persistHiddenOnServer(
externalRoomId: string,
messageIds: string[],
forEveryone = true,
) {
const ids = [...new Set(messageIds.map((id) => String(id || '').trim()).filter(Boolean))];
if (!externalRoomId || !ids.length) return ids;
rememberHidden(externalRoomId, ids);
@@ -102,6 +106,7 @@ async function persistHiddenOnServer(externalRoomId: string, messageIds: string[
await api.post('/conversations/messages/hide', {
externalRoomId,
messageIds: ids,
forEveryone,
});
} catch {
// local purge already applied; sync may retry via poll
@@ -726,7 +731,7 @@ function onSocketMessage(event: MessageEvent) {
const ids = [hideTarget, purgeId].filter(Boolean);
purgeMessageIds(ids);
rememberHidden(roomId, ids);
void persistHiddenOnServer(roomId, ids);
void persistHiddenOnServer(roomId, ids, true);
return;
}
if (purgeId && isDeletedContent(purgeContent)) {
@@ -1196,27 +1201,34 @@ export function useOrgChat() {
});
}
async function deleteMessage(roomId: string, messageId: string) {
async function deleteMessage(
roomId: string,
messageId: string,
opts?: { forEveryone?: boolean },
) {
if (!messageId) return;
await connectSocket();
if (!socket || socket.readyState !== WebSocket.OPEN) throw new Error('ws not open');
const forEveryone = opts?.forEveryone !== false;
// 1) Persist hide in CRM (survives refresh for everyone)
await persistHiddenOnServer(roomId, [messageId]);
// 1) Persist hide in CRM
await persistHiddenOnServer(roomId, [messageId], forEveryone);
// 2) Realtime: send a normal text command (text delivery is reliable on chat WS)
const controlId = crypto.randomUUID();
rememberHidden(roomId, [controlId]);
socket.send(
JSON.stringify({
id: controlId,
type: 'text',
room_id: roomId,
content: `${HIDE_CMD_PREFIX}${messageId}`,
}),
);
// Hide the control message itself so it never appears in history UI
void persistHiddenOnServer(roomId, [controlId]);
// 2) Realtime sync only when deleting for everyone
if (forEveryone) {
await connectSocket();
if (socket && socket.readyState === WebSocket.OPEN) {
const controlId = crypto.randomUUID();
rememberHidden(roomId, [controlId]);
socket.send(
JSON.stringify({
id: controlId,
type: 'text',
room_id: roomId,
content: `${HIDE_CMD_PREFIX}${messageId}`,
}),
);
void persistHiddenOnServer(roomId, [controlId], true);
}
}
const last = [...messages.value].reverse().find((m) => m.roomId === roomId);
emitRoomLive({
@@ -1227,10 +1239,16 @@ export function useOrgChat() {
});
}
async function deleteMessages(roomId: string, messageIds: string[]) {
const ids = [...new Set(messageIds.filter(Boolean))];
for (const id of ids) {
await deleteMessage(roomId, id);
async function deleteMessages(
roomId: string,
items: Array<{ id: string; forEveryone?: boolean }>,
) {
const seen = new Set<string>();
for (const item of items) {
const id = String(item.id || '').trim();
if (!id || seen.has(id)) continue;
seen.add(id);
await deleteMessage(roomId, id, { forEveryone: item.forEveryone !== false });
}
}
+103 -46
View File
@@ -46,7 +46,7 @@ const recording = ref(false);
const recordSecs = ref(0);
const lightboxUrl = ref('');
const fileInputEl = ref<HTMLInputElement | null>(null);
const draftInputEl = 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);
@@ -194,11 +194,19 @@ function selectedMessages(): ChatMessage[] {
async function deleteSelected() {
if (!activeRoom.value || !selectedIds.value.length) return;
const n = selectedIds.value.length;
if (!window.confirm(n > 1 ? `${n} پیام حذف شوند؟` : 'این پیام حذف شود؟')) return;
const ids = [...selectedIds.value];
const list = selectedMessages();
if (!list.length) return;
const mineCount = list.filter((m) => isMineMessage(m)).length;
const otherCount = list.length - mineCount;
const parts: string[] = [];
if (mineCount) parts.push(`${mineCount} پیام خودتان برای همه`);
if (otherCount) parts.push(`${otherCount} پیام دریافتی فقط برای شما`);
if (!window.confirm(`حذف شود؟\n${parts.join('\n')}`)) return;
try {
await deleteMessages(activeRoom.value.externalRoomId, ids);
await deleteMessages(
activeRoom.value.externalRoomId,
list.map((m) => ({ id: m.id, forEveryone: isMineMessage(m) })),
);
exitSelectMode();
} catch (e) {
error.value = e instanceof Error ? e.message : 'حذف پیام ناموفق بود.';
@@ -266,15 +274,49 @@ function startEditMessage(msg: ChatMessage) {
async function confirmDeleteMessage(msg: ChatMessage) {
msgMenu.value = null;
if (!activeRoom.value || msg.optimistic || msg.deleted) return;
if (!window.confirm('این پیام برای همه حذف شود؟')) return;
const mine = isMineMessage(msg);
const ok = window.confirm(
mine
? 'این پیام برای همه حذف شود؟'
: 'این پیام فقط برای شما حذف شود؟ (برای طرف مقابل باقی می‌ماند)',
);
if (!ok) return;
try {
await deleteMessage(activeRoom.value.externalRoomId, msg.id);
await deleteMessage(activeRoom.value.externalRoomId, msg.id, {
forEveryone: mine,
});
if (editingMessageId.value === msg.id) cancelEdit();
} catch (e) {
error.value = e instanceof Error ? e.message : 'حذف پیام ناموفق بود.';
}
}
function onComposerKeydown(event: KeyboardEvent) {
if (event.key !== 'Enter') return;
if (event.ctrlKey || event.metaKey) {
// Ctrl/Cmd+Enter → newline
event.preventDefault();
const el = draftInputEl.value;
if (!el) {
draft.value += '\n';
return;
}
const start = el.selectionStart ?? draft.value.length;
const end = el.selectionEnd ?? start;
const next = draft.value.slice(0, start) + '\n' + draft.value.slice(end);
draft.value = next;
nextTick(() => {
const pos = start + 1;
el.focus();
el.setSelectionRange(pos, pos);
});
return;
}
if (event.shiftKey) return;
event.preventDefault();
void submitMessage();
}
function openMsgMenu(msg: ChatMessage, event: MouseEvent | TouchEvent) {
if (msg.deleted || msg.optimistic) return;
if (selectMode.value) {
@@ -966,38 +1008,6 @@ onUnmounted(() => {
accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.zip,.rar,.txt"
@change="onFilePicked"
/>
<button
type="button"
class="tool-btn attach-btn"
title="پیوست"
:disabled="sending || recording || Boolean(editingMessageId)"
@click="fileInputEl?.click()"
>
<UiIcon name="plus" :size="20" />
</button>
<div class="input-shell">
<button
type="button"
class="emoji-toggle"
title="ایموجی"
:class="{ on: emojiOpen }"
:disabled="sending || recording"
@click="emojiOpen = !emojiOpen"
>
😊
</button>
<input
ref="draftInputEl"
v-model="draft"
type="text"
class="composer-input"
:placeholder="editingMessageId ? 'ویرایش پیام…' : 'پیام بنویسید…'"
autocomplete="off"
enterkeyhint="send"
:disabled="sending || recording"
@focus="emojiOpen = false"
/>
</div>
<button
v-if="showMicInsteadOfSend"
type="button"
@@ -1017,6 +1027,39 @@ onUnmounted(() => {
>
<UiIcon name="send" :size="18" />
</button>
<div class="input-shell">
<textarea
ref="draftInputEl"
v-model="draft"
class="composer-input"
rows="1"
:placeholder="editingMessageId ? 'ویرایش پیام…' : 'پیام بنویسید…'"
autocomplete="off"
enterkeyhint="send"
:disabled="sending || recording"
@focus="emojiOpen = false"
@keydown="onComposerKeydown"
/>
</div>
<button
type="button"
class="emoji-toggle"
title="ایموجی"
:class="{ on: emojiOpen }"
:disabled="sending || recording"
@click="emojiOpen = !emojiOpen"
>
😊
</button>
<button
type="button"
class="tool-btn attach-btn"
title="پیوست"
:disabled="sending || recording || Boolean(editingMessageId)"
@click="fileInputEl?.click()"
>
<UiIcon name="plus" :size="20" />
</button>
</div>
</form>
@@ -1062,6 +1105,15 @@ onUnmounted(() => {
<UiIcon name="trash" :size="16" />
حذف برای همه
</button>
<button
v-else
type="button"
class="danger"
@click="confirmDeleteMessage(menuMessage()!)"
>
<UiIcon name="trash" :size="16" />
حذف فقط برای من
</button>
</div>
</template>
</section>
@@ -1604,12 +1656,11 @@ onUnmounted(() => {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: center;
gap: 0.15rem;
align-items: flex-end;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 22px;
padding: 0.15rem 0.35rem 0.15rem 0.55rem;
padding: 0.35rem 0.75rem;
min-height: 2.75rem;
}
.composer-input {
@@ -1618,17 +1669,22 @@ onUnmounted(() => {
border: none;
outline: none;
border-radius: 18px;
padding: 0.55rem 0.35rem;
padding: 0.35rem 0;
font: inherit;
font-size: 0.95rem;
background: transparent;
resize: none;
max-height: 7.5rem;
line-height: 1.45;
field-sizing: content;
}
.emoji-toggle {
flex: 0 0 auto;
border: none;
background: transparent;
width: 2.1rem;
height: 2.1rem;
background: #fff;
border: 1px solid #e2e8f0;
width: 2.75rem;
height: 2.75rem;
border-radius: 999px;
font-size: 1.15rem;
line-height: 1;
@@ -1638,6 +1694,7 @@ onUnmounted(() => {
}
.emoji-toggle.on {
background: rgba(13, 148, 136, 0.12);
border-color: rgba(13, 148, 136, 0.35);
}
.emoji-panel {
display: grid;