Aligning the chat bar with Telegram

This commit is contained in:
2026-09-29 21:06:04 +03:30
parent e20b904bbe
commit c20dc0a7ca
2 changed files with 536 additions and 42 deletions
+150 -4
View File
@@ -22,6 +22,8 @@ export type ChatMessage = {
fileName?: string;
uploadProgress?: number;
localPreviewUrl?: string;
edited?: boolean;
deleted?: boolean;
};
export type RoomLiveEvent = {
@@ -299,6 +301,13 @@ 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 ||
/^(?:\[?\s*deleted\s*\]?|این پیام حذف شد|پیام حذف شد)$/i.test(content),
);
const edited = Boolean(base.edited || base.is_edited || base.isEdited || base.edited_at);
return {
id: id || `tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -306,16 +315,18 @@ function normalizeIncoming(
senderId: String(
base.sender_id ?? base.user_id ?? base.from_user_id ?? raw.sender_id ?? '',
),
content,
type,
content: deleted ? 'این پیام حذف شد' : content,
type: deleted ? 'text' : 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,
fileName,
mediaUrl: deleted ? undefined : mediaUrl,
fileName: deleted ? undefined : fileName,
edited: deleted ? false : edited,
deleted,
};
}
@@ -340,6 +351,8 @@ function mergeMessages(incoming: ChatMessage, list: ChatMessage[]): ChatMessage[
localPreviewUrl: incoming.localPreviewUrl ?? prev.localPreviewUrl,
mediaUrl: incoming.mediaUrl || prev.mediaUrl,
fileName: incoming.fileName || prev.fileName,
edited: incoming.edited || prev.edited,
deleted: incoming.deleted || prev.deleted,
delivery:
incoming.delivery === 'read' || prev.delivery === 'read'
? 'read'
@@ -379,6 +392,8 @@ function mergeMessages(incoming: ChatMessage, list: ChatMessage[]): ChatMessage[
uploadProgress: undefined,
mediaUrl: incoming.mediaUrl || prev.mediaUrl,
fileName: incoming.fileName || prev.fileName,
edited: incoming.edited || prev.edited,
deleted: incoming.deleted || prev.deleted,
delivery: incoming.readAt ? 'read' : 'sent',
};
if (prev.localPreviewUrl?.startsWith('blob:')) {
@@ -496,6 +511,53 @@ function handleIncomingMessage(msg: ChatMessage) {
});
}
function applyEditedMessage(payload: Record<string, unknown>) {
const base =
payload.message && typeof payload.message === 'object'
? (payload.message as Record<string, unknown>)
: payload;
const id = String(base.id ?? base.message_id ?? payload.message_id ?? '').trim();
if (!id) return;
const content = String(base.content ?? base.text ?? base.body ?? '').trim();
if (!content) return;
messages.value = messages.value.map((m) =>
m.id === id
? {
...m,
content,
type: 'text',
edited: true,
deleted: false,
mediaUrl: undefined,
fileName: undefined,
}
: m,
);
}
function applyDeletedMessage(payload: Record<string, unknown>) {
const base =
payload.message && typeof payload.message === 'object'
? (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: 'این پیام حذف شد',
type: 'text',
deleted: true,
edited: false,
mediaUrl: undefined,
fileName: undefined,
localPreviewUrl: undefined,
}
: m,
);
}
function onSocketMessage(event: MessageEvent) {
const raw = String(event.data ?? '').trim();
if (!raw || raw === 'pong' || raw === 'ping') return;
@@ -507,6 +569,28 @@ function onSocketMessage(event: MessageEvent) {
applySeenPayload(parsed);
return;
}
if (
type === 'edit' ||
type === 'edited' ||
type === 'update' ||
type === 'message_edit' ||
type === 'message:edit' ||
type === 'message_edited'
) {
applyEditedMessage(parsed);
return;
}
if (
type === 'delete' ||
type === 'deleted' ||
type === 'remove' ||
type === 'message_delete' ||
type === 'message:delete' ||
type === 'message_deleted'
) {
applyDeletedMessage(parsed);
return;
}
const msg = normalizeIncoming(parsed, activeRoomId.value);
if (!msg) return;
handleIncomingMessage(msg);
@@ -916,6 +1000,66 @@ export function useOrgChat() {
}
}
async function editMessage(roomId: string, messageId: string, text: string) {
const trimmed = text.trim();
if (!trimmed || !messageId) return;
await connectSocket();
if (!socket || socket.readyState !== WebSocket.OPEN) throw new Error('ws not open');
messages.value = messages.value.map((m) =>
m.id === messageId
? {
...m,
content: trimmed,
type: 'text',
edited: true,
deleted: false,
mediaUrl: undefined,
fileName: undefined,
}
: m,
);
socket.send(
JSON.stringify({
type: 'edit',
id: messageId,
room_id: roomId,
content: trimmed,
}),
);
emitRoomLive({
roomId,
preview: trimmed,
at: new Date().toISOString(),
fromSelf: true,
});
}
async function deleteMessage(roomId: string, messageId: string) {
if (!messageId) return;
await connectSocket();
if (!socket || socket.readyState !== WebSocket.OPEN) throw new Error('ws not open');
applyDeletedMessage({ id: messageId });
socket.send(
JSON.stringify({
type: 'delete',
id: messageId,
room_id: roomId,
}),
);
emitRoomLive({
roomId,
preview: 'این پیام حذف شد',
at: new Date().toISOString(),
fromSelf: true,
});
}
function clearActiveRoom() {
activeRoomId.value = '';
messages.value = [];
@@ -937,6 +1081,8 @@ export function useOrgChat() {
loadMessages,
sendText,
sendFile,
editMessage,
deleteMessage,
sendRead,
ensureConnected,
setConversationsPageOpen,