Add, edit, delete, and resend messages.

This commit is contained in:
2026-09-29 21:26:07 +03:30
parent c20dc0a7ca
commit 83504816c5
2 changed files with 362 additions and 33 deletions
+78 -16
View File
@@ -37,6 +37,11 @@ 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 = 'این پیام حذف شد';
function isDeletedContent(content: string): boolean {
return /^(?:\[?\s*deleted\s*\]?|این پیام حذف شد|پیام حذف شد)$/i.test(content.trim());
}
let socket: WebSocket | null = null;
let sessionCache: ConversationSessionDto | null = null;
@@ -305,7 +310,7 @@ function normalizeIncoming(
base.deleted ||
base.is_deleted ||
base.isDeleted ||
/^(?:\[?\s*deleted\s*\]?|این پیام حذف شد|پیام حذف شد)$/i.test(content),
isDeletedContent(content),
);
const edited = Boolean(base.edited || base.is_edited || base.isEdited || base.edited_at);
@@ -315,7 +320,7 @@ function normalizeIncoming(
senderId: String(
base.sender_id ?? base.user_id ?? base.from_user_id ?? raw.sender_id ?? '',
),
content: deleted ? 'این پیام حذف شد' : content,
content: deleted ? DELETED_CONTENT : content,
type: deleted ? 'text' : type,
createdAt: String(
base.created_at ?? base.createdAt ?? raw.created_at ?? new Date().toISOString(),
@@ -339,20 +344,33 @@ function mergeMessages(incoming: ChatMessage, list: ChatMessage[]): ChatMessage[
...prev,
...incoming,
optimistic: false,
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,
),
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,
localPreviewUrl: incoming.localPreviewUrl ?? prev.localPreviewUrl,
mediaUrl: incoming.mediaUrl || prev.mediaUrl,
fileName: incoming.fileName || prev.fileName,
mediaUrl:
prev.deleted || incoming.deleted
? undefined
: incoming.mediaUrl || prev.mediaUrl,
fileName:
prev.deleted || incoming.deleted
? undefined
: incoming.fileName || prev.fileName,
edited: incoming.edited || prev.edited,
deleted: incoming.deleted || prev.deleted,
deleted: prev.deleted || incoming.deleted,
delivery:
incoming.delivery === 'read' || prev.delivery === 'read'
? 'read'
@@ -520,6 +538,11 @@ 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)
if (isDeletedContent(content)) {
applyDeletedMessage({ id });
return;
}
messages.value = messages.value.map((m) =>
m.id === id
? {
@@ -546,7 +569,7 @@ function applyDeletedMessage(payload: Record<string, unknown>) {
m.id === id
? {
...m,
content: 'این پیام حذف شد',
content: DELETED_CONTENT,
type: 'text',
deleted: true,
edited: false,
@@ -1044,22 +1067,59 @@ export function useOrgChat() {
applyDeletedMessage({ id: messageId });
// Chat WS does not reliably broadcast type:delete — sync via edit (proven channel).
socket.send(
JSON.stringify({
type: 'edit',
id: messageId,
room_id: roomId,
content: DELETED_CONTENT,
}),
);
// Best-effort native delete for backends that support it
socket.send(
JSON.stringify({
type: 'delete',
id: messageId,
message_id: messageId,
room_id: roomId,
}),
);
emitRoomLive({
roomId,
preview: 'این پیام حذف شد',
preview: DELETED_CONTENT,
at: new Date().toISOString(),
fromSelf: true,
});
}
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 forwardMessages(targetRoomId: string, sourceMessages: ChatMessage[]) {
const roomId = targetRoomId.trim();
if (!roomId) return;
await connectSocket();
for (const msg of sourceMessages) {
if (!msg || msg.deleted || msg.optimistic) continue;
if (msg.type === 'text') {
const text = msg.content.trim();
if (text && !isDeletedContent(text)) await sendText(roomId, text);
continue;
}
const path = String(msg.content || '').trim();
if (!path) continue;
await sendMediaPath(roomId, path, msg.type, {
fileName: msg.fileName,
});
}
}
function clearActiveRoom() {
activeRoomId.value = '';
messages.value = [];
@@ -1083,6 +1143,8 @@ export function useOrgChat() {
sendFile,
editMessage,
deleteMessage,
deleteMessages,
forwardMessages,
sendRead,
ensureConnected,
setConversationsPageOpen,