feat(web): add telegram-like message status indicators
All checks were successful
CI / test (push) Successful in 21s
All checks were successful
CI / test (push) Successful in 21s
- optimistic sending state with pending clock icon - transition statuses sent -> delivered -> read via realtime events - render checkmarks next to outgoing message timestamps
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { getChats, getMessages, updateMessageStatus } from "../api/chats";
|
||||
import type { Chat, Message } from "../chat/types";
|
||||
import type { Chat, DeliveryStatus, Message, MessageType } from "../chat/types";
|
||||
|
||||
interface ChatState {
|
||||
chats: Chat[];
|
||||
@@ -11,6 +11,16 @@ interface ChatState {
|
||||
setActiveChatId: (chatId: number | null) => void;
|
||||
loadMessages: (chatId: number) => Promise<void>;
|
||||
prependMessage: (chatId: number, message: Message) => void;
|
||||
addOptimisticMessage: (params: {
|
||||
chatId: number;
|
||||
senderId: number;
|
||||
type: MessageType;
|
||||
text: string | null;
|
||||
clientMessageId: string;
|
||||
}) => void;
|
||||
confirmMessageByClientId: (chatId: number, clientMessageId: string, message: Message) => void;
|
||||
removeOptimisticMessage: (chatId: number, clientMessageId: string) => void;
|
||||
setMessageDeliveryStatus: (chatId: number, messageId: number, status: DeliveryStatus) => void;
|
||||
setTypingUsers: (chatId: number, userIds: number[]) => void;
|
||||
}
|
||||
|
||||
@@ -40,13 +50,87 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
},
|
||||
prependMessage: (chatId, message) => {
|
||||
const old = get().messagesByChat[chatId] ?? [];
|
||||
if (old.some((m) => m.id === message.id)) {
|
||||
if (old.some((m) => m.id === message.id || (message.client_message_id && m.client_message_id === message.client_message_id))) {
|
||||
return;
|
||||
}
|
||||
set((state) => ({
|
||||
messagesByChat: { ...state.messagesByChat, [chatId]: [...old, message] }
|
||||
}));
|
||||
},
|
||||
addOptimisticMessage: ({ chatId, senderId, type, text, clientMessageId }) => {
|
||||
const old = get().messagesByChat[chatId] ?? [];
|
||||
if (old.some((m) => m.client_message_id === clientMessageId)) {
|
||||
return;
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
const optimistic: Message = {
|
||||
id: -(Date.now() + Math.floor(Math.random() * 10000)),
|
||||
chat_id: chatId,
|
||||
sender_id: senderId,
|
||||
type,
|
||||
text,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
client_message_id: clientMessageId,
|
||||
delivery_status: "sending",
|
||||
is_pending: true
|
||||
};
|
||||
set((state) => ({
|
||||
messagesByChat: { ...state.messagesByChat, [chatId]: [...old, optimistic] }
|
||||
}));
|
||||
},
|
||||
confirmMessageByClientId: (chatId, clientMessageId, message) => {
|
||||
const old = get().messagesByChat[chatId] ?? [];
|
||||
const idx = old.findIndex((m) => m.client_message_id === clientMessageId);
|
||||
if (idx === -1) {
|
||||
if (!old.some((m) => m.id === message.id)) {
|
||||
set((state) => ({
|
||||
messagesByChat: {
|
||||
...state.messagesByChat,
|
||||
[chatId]: [...old, { ...message, client_message_id: clientMessageId, delivery_status: "sent", is_pending: false }]
|
||||
}
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const next = [...old];
|
||||
next[idx] = {
|
||||
...message,
|
||||
client_message_id: clientMessageId,
|
||||
delivery_status: "sent",
|
||||
is_pending: false
|
||||
};
|
||||
set((state) => ({
|
||||
messagesByChat: { ...state.messagesByChat, [chatId]: next }
|
||||
}));
|
||||
},
|
||||
removeOptimisticMessage: (chatId, clientMessageId) => {
|
||||
const old = get().messagesByChat[chatId] ?? [];
|
||||
set((state) => ({
|
||||
messagesByChat: {
|
||||
...state.messagesByChat,
|
||||
[chatId]: old.filter((m) => m.client_message_id !== clientMessageId)
|
||||
}
|
||||
}));
|
||||
},
|
||||
setMessageDeliveryStatus: (chatId, messageId, status) => {
|
||||
const old = get().messagesByChat[chatId] ?? [];
|
||||
const idx = old.findIndex((m) => m.id === messageId);
|
||||
if (idx === -1) {
|
||||
return;
|
||||
}
|
||||
const current = old[idx];
|
||||
const order: Record<DeliveryStatus, number> = { sending: 1, sent: 2, delivered: 3, read: 4 };
|
||||
const currentStatus = current.delivery_status ?? "sent";
|
||||
if (order[status] <= order[currentStatus]) {
|
||||
return;
|
||||
}
|
||||
const next = [...old];
|
||||
next[idx] = { ...current, delivery_status: status, is_pending: false };
|
||||
set((state) => ({
|
||||
messagesByChat: { ...state.messagesByChat, [chatId]: next }
|
||||
}));
|
||||
},
|
||||
setTypingUsers: (chatId, userIds) =>
|
||||
set((state) => ({ typingByChat: { ...state.typingByChat, [chatId]: userIds } }))
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user