Fixing the complete removal
This commit is contained in:
+102
-61
@@ -37,10 +37,23 @@ const PAGE_SIZE = 50;
|
||||
const IMAGE_MAX_EDGE = 1600;
|
||||
const IMAGE_WEBP_QUALITY = 0.82;
|
||||
const MAX_UPLOAD_BYTES = 40 * 1024 * 1024;
|
||||
const DELETED_CONTENT = 'این پیام حذف شد';
|
||||
/** Synced hard-delete marker (edit channel). Clients purge the message; never shown in UI. */
|
||||
const HARD_DELETE_MARKER = '__MENTO_DEL__';
|
||||
|
||||
function isDeletedContent(content: string): boolean {
|
||||
return /^(?:\[?\s*deleted\s*\]?|این پیام حذف شد|پیام حذف شد)$/i.test(content.trim());
|
||||
const t = content.trim();
|
||||
return (
|
||||
/^(?:\[?\s*deleted\s*\]?|این پیام حذف شد|پیام حذف شد)$/i.test(t) ||
|
||||
t.includes('MENTO_DEL') ||
|
||||
t === HARD_DELETE_MARKER
|
||||
);
|
||||
}
|
||||
|
||||
function purgeMessageById(id: string) {
|
||||
const key = String(id || '').trim();
|
||||
if (!key) return;
|
||||
const next = messages.value.filter((m) => m.id !== key);
|
||||
if (next.length !== messages.value.length) messages.value = next;
|
||||
}
|
||||
|
||||
let socket: WebSocket | null = null;
|
||||
@@ -306,12 +319,11 @@ function normalizeIncoming(
|
||||
const type = inferMediaType(declared, content, fileName);
|
||||
const isMedia = type !== 'text';
|
||||
const mediaUrl = isMedia ? buildChatFileUrl(content, downloadUrl) : undefined;
|
||||
const deleted = Boolean(
|
||||
base.deleted ||
|
||||
base.is_deleted ||
|
||||
base.isDeleted ||
|
||||
isDeletedContent(content),
|
||||
);
|
||||
const deletedFlag = Boolean(base.deleted || base.is_deleted || base.isDeleted);
|
||||
if (deletedFlag || isDeletedContent(content)) {
|
||||
// Hard-delete: drop from history / ignore tombstones
|
||||
return null;
|
||||
}
|
||||
const edited = Boolean(base.edited || base.is_edited || base.isEdited || base.edited_at);
|
||||
|
||||
return {
|
||||
@@ -320,57 +332,50 @@ function normalizeIncoming(
|
||||
senderId: String(
|
||||
base.sender_id ?? base.user_id ?? base.from_user_id ?? raw.sender_id ?? '',
|
||||
),
|
||||
content: deleted ? DELETED_CONTENT : content,
|
||||
type: deleted ? 'text' : type,
|
||||
content,
|
||||
type,
|
||||
createdAt: String(
|
||||
base.created_at ?? base.createdAt ?? raw.created_at ?? new Date().toISOString(),
|
||||
),
|
||||
reply: base.reply ? String(base.reply) : null,
|
||||
readAt: readAt ? String(readAt) : null,
|
||||
delivery: readAt ? 'read' : 'sent',
|
||||
mediaUrl: deleted ? undefined : mediaUrl,
|
||||
fileName: deleted ? undefined : fileName,
|
||||
edited: deleted ? false : edited,
|
||||
deleted,
|
||||
mediaUrl,
|
||||
fileName,
|
||||
edited,
|
||||
deleted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeMessages(incoming: ChatMessage, list: ChatMessage[]): ChatMessage[] {
|
||||
if (incoming.deleted || isDeletedContent(incoming.content)) {
|
||||
return list.filter((m) => m.id !== incoming.id);
|
||||
}
|
||||
const byId = list.findIndex((m) => m.id === incoming.id);
|
||||
if (byId >= 0) {
|
||||
const prev = list[byId];
|
||||
if (prev.deleted) {
|
||||
return list.filter((m) => m.id !== prev.id);
|
||||
}
|
||||
const next = [...list];
|
||||
next[byId] = {
|
||||
...prev,
|
||||
...incoming,
|
||||
optimistic: false,
|
||||
type:
|
||||
prev.deleted || incoming.deleted
|
||||
? 'text'
|
||||
: inferMediaType(
|
||||
prev.type === 'voice' || incoming.type === 'voice'
|
||||
? 'voice'
|
||||
: prev.type === 'video' || incoming.type === 'video'
|
||||
? 'video'
|
||||
: incoming.type,
|
||||
incoming.content || prev.content,
|
||||
incoming.fileName || prev.fileName,
|
||||
),
|
||||
content:
|
||||
prev.deleted || incoming.deleted
|
||||
? DELETED_CONTENT
|
||||
: incoming.content || prev.content,
|
||||
type: inferMediaType(
|
||||
prev.type === 'voice' || incoming.type === 'voice'
|
||||
? 'voice'
|
||||
: prev.type === 'video' || incoming.type === 'video'
|
||||
? 'video'
|
||||
: incoming.type,
|
||||
incoming.content || prev.content,
|
||||
incoming.fileName || prev.fileName,
|
||||
),
|
||||
localPreviewUrl: incoming.localPreviewUrl ?? prev.localPreviewUrl,
|
||||
mediaUrl:
|
||||
prev.deleted || incoming.deleted
|
||||
? undefined
|
||||
: incoming.mediaUrl || prev.mediaUrl,
|
||||
fileName:
|
||||
prev.deleted || incoming.deleted
|
||||
? undefined
|
||||
: incoming.fileName || prev.fileName,
|
||||
mediaUrl: incoming.mediaUrl || prev.mediaUrl,
|
||||
fileName: incoming.fileName || prev.fileName,
|
||||
edited: incoming.edited || prev.edited,
|
||||
deleted: prev.deleted || incoming.deleted,
|
||||
deleted: false,
|
||||
delivery:
|
||||
incoming.delivery === 'read' || prev.delivery === 'read'
|
||||
? 'read'
|
||||
@@ -538,9 +543,9 @@ function applyEditedMessage(payload: Record<string, unknown>) {
|
||||
if (!id) return;
|
||||
const content = String(base.content ?? base.text ?? base.body ?? '').trim();
|
||||
if (!content) return;
|
||||
// Delete-for-everyone is synced via edit tombstone (native delete is not broadcast by chat WS)
|
||||
// Hard-delete sync uses edit channel with purge marker
|
||||
if (isDeletedContent(content)) {
|
||||
applyDeletedMessage({ id });
|
||||
purgeMessageById(id);
|
||||
return;
|
||||
}
|
||||
messages.value = messages.value.map((m) =>
|
||||
@@ -564,21 +569,25 @@ function applyDeletedMessage(payload: Record<string, unknown>) {
|
||||
? (payload.message as Record<string, unknown>)
|
||||
: payload;
|
||||
const id = String(base.id ?? base.message_id ?? payload.message_id ?? '').trim();
|
||||
if (!id) return;
|
||||
messages.value = messages.value.map((m) =>
|
||||
m.id === id
|
||||
? {
|
||||
...m,
|
||||
content: DELETED_CONTENT,
|
||||
type: 'text',
|
||||
deleted: true,
|
||||
edited: false,
|
||||
mediaUrl: undefined,
|
||||
fileName: undefined,
|
||||
localPreviewUrl: undefined,
|
||||
}
|
||||
: m,
|
||||
);
|
||||
purgeMessageById(id);
|
||||
}
|
||||
|
||||
function extractPayloadContent(parsed: Record<string, unknown>): string {
|
||||
const base =
|
||||
parsed.message && typeof parsed.message === 'object'
|
||||
? (parsed.message as Record<string, unknown>)
|
||||
: parsed;
|
||||
return String(
|
||||
base.content ?? base.text ?? base.body ?? parsed.content ?? '',
|
||||
).trim();
|
||||
}
|
||||
|
||||
function extractPayloadId(parsed: Record<string, unknown>): string {
|
||||
const base =
|
||||
parsed.message && typeof parsed.message === 'object'
|
||||
? (parsed.message as Record<string, unknown>)
|
||||
: parsed;
|
||||
return String(base.id ?? base.message_id ?? parsed.message_id ?? '').trim();
|
||||
}
|
||||
|
||||
function onSocketMessage(event: MessageEvent) {
|
||||
@@ -614,6 +623,12 @@ function onSocketMessage(event: MessageEvent) {
|
||||
applyDeletedMessage(parsed);
|
||||
return;
|
||||
}
|
||||
const purgeId = extractPayloadId(parsed);
|
||||
const purgeContent = extractPayloadContent(parsed);
|
||||
if (purgeId && isDeletedContent(purgeContent)) {
|
||||
purgeMessageById(purgeId);
|
||||
return;
|
||||
}
|
||||
const msg = normalizeIncoming(parsed, activeRoomId.value);
|
||||
if (!msg) return;
|
||||
handleIncomingMessage(msg);
|
||||
@@ -1065,18 +1080,18 @@ export function useOrgChat() {
|
||||
await connectSocket();
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) throw new Error('ws not open');
|
||||
|
||||
applyDeletedMessage({ id: messageId });
|
||||
// Remove locally immediately (no tombstone)
|
||||
purgeMessageById(messageId);
|
||||
|
||||
// Chat WS does not reliably broadcast type:delete — sync via edit (proven channel).
|
||||
// Sync to peers via edit channel (proven to broadcast) + best-effort delete
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: 'edit',
|
||||
id: messageId,
|
||||
room_id: roomId,
|
||||
content: DELETED_CONTENT,
|
||||
content: HARD_DELETE_MARKER,
|
||||
}),
|
||||
);
|
||||
// Best-effort native delete for backends that support it
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: 'delete',
|
||||
@@ -1085,10 +1100,36 @@ export function useOrgChat() {
|
||||
room_id: roomId,
|
||||
}),
|
||||
);
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: 'remove',
|
||||
id: messageId,
|
||||
message_id: messageId,
|
||||
room_id: roomId,
|
||||
}),
|
||||
);
|
||||
|
||||
// Best-effort REST delete if the chat host supports it
|
||||
try {
|
||||
const session = await ensureSession();
|
||||
const apiKey = new URL(session.wsUrl).searchParams.get('api_key') ?? '';
|
||||
const params = new URLSearchParams({
|
||||
api_key: apiKey,
|
||||
room_id: roomId,
|
||||
user_id: session.chatUserId,
|
||||
});
|
||||
void fetch(`${session.messagesBaseUrl}/api/messages/${encodeURIComponent(messageId)}?${params}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Accept: 'application/json' },
|
||||
}).catch(() => undefined);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const last = [...messages.value].reverse().find((m) => m.roomId === roomId);
|
||||
emitRoomLive({
|
||||
roomId,
|
||||
preview: DELETED_CONTENT,
|
||||
preview: last ? previewLabelForMessage(last) : ' ',
|
||||
at: new Date().toISOString(),
|
||||
fromSelf: true,
|
||||
});
|
||||
|
||||
@@ -837,18 +837,14 @@ onUnmounted(() => {
|
||||
<div
|
||||
class="bubble"
|
||||
:class="{
|
||||
media: msg.type !== 'text' && !msg.deleted,
|
||||
deleted: msg.deleted,
|
||||
media: msg.type !== 'text',
|
||||
}"
|
||||
@touchstart.passive="onBubbleTouchStart(msg, $event)"
|
||||
@touchend="onBubbleTouchEnd"
|
||||
@touchmove="onBubbleTouchEnd"
|
||||
@touchcancel="onBubbleTouchEnd"
|
||||
>
|
||||
<template v-if="msg.deleted">
|
||||
<p class="deleted-text">این پیام حذف شد</p>
|
||||
</template>
|
||||
<template v-else-if="msg.type === 'image'">
|
||||
<template v-if="msg.type === 'image'">
|
||||
<button
|
||||
type="button"
|
||||
class="media-image"
|
||||
@@ -907,7 +903,7 @@ onUnmounted(() => {
|
||||
</template>
|
||||
<p v-else>{{ msg.content }}</p>
|
||||
<div class="bubble-meta">
|
||||
<span v-if="msg.edited && !msg.deleted" class="edited-mark">ویرایششده</span>
|
||||
<span v-if="msg.edited" class="edited-mark">ویرایششده</span>
|
||||
<time>{{ formatListTime(msg.createdAt) }}</time>
|
||||
<span
|
||||
v-if="msg.senderId === chatUserId || msg.optimistic"
|
||||
|
||||
Reference in New Issue
Block a user