From a08c41067edcf45c7f8de997c23c8b6f2339528a Mon Sep 17 00:00:00 2001 From: Alois Date: Tue, 7 Jul 2026 18:21:52 +0200 Subject: [PATCH] (feat): unify message patching into a single interface --- packages/chat/src/components/message.tsx | 19 +-- packages/chat/src/context.tsx | 200 +++++++---------------- 2 files changed, 65 insertions(+), 154 deletions(-) diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index aa050fd..997f42f 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -143,7 +143,7 @@ function MessageComponent({ }, [message.Content]); // Message editing - const { chatSecret, editMessageContent, userId } = useChat(); + const { chatSecret, editMessage, userId } = useChat(); const [editing, setEditing] = useState(false); const [editDraft, setEditDraft] = useState(message.Content); useEffect(() => { @@ -151,7 +151,7 @@ function MessageComponent({ setEditDraft(message.Content); } }, [editing, message.Content]); - const editMessage = useCallback( + const submitEditMessage = useCallback( async (newContent: string) => { if (!chatSecret) return; @@ -165,7 +165,7 @@ function MessageComponent({ if (!encryptedContent) return; const previousContent = message.Content; - editMessageContent(message.SendTime, newContent); + editMessage(message.SendTime, { Content: newContent, Edited: true }); try { const response = await send("MessageEdit", { @@ -178,18 +178,17 @@ function MessageComponent({ throw new Error(response.type); } } catch (err) { - editMessageContent( - message.SendTime, - previousContent, - message.Edited ?? false, - ); + editMessage(message.SendTime, { + Content: previousContent, + Edited: message.Edited ?? false, + }); log(1, "chat", "red", "Failed to edit message", err); toast("error", "Failed to edit message", String(err)); } }, [ chatSecret, - editMessageContent, + editMessage, userId, message.Content, message.Edited, @@ -288,7 +287,7 @@ function MessageComponent({ setValue={setEditDraft} value={editDraft} onSubmit={() => { - editMessage(editDraft); + submitEditMessage(editDraft); setEditing(false); }} /> diff --git a/packages/chat/src/context.tsx b/packages/chat/src/context.tsx index a2ed7b3..af0226f 100644 --- a/packages/chat/src/context.tsx +++ b/packages/chat/src/context.tsx @@ -59,40 +59,15 @@ function bytesFromProtocol(value: unknown): Uint8Array { throw new Error("expected protocol bytes"); } -function updateMessageStateBySendTime< - T extends { SendTime: number; MessageState: RawMessage["MessageState"] }, ->( +type EditableMessage = RawMessage & { failed?: boolean }; +type MessageEdit = Partial< + Pick +>; + +function updateMessagesBySendTime( messages: T[], sendTime: number, - messageState: RawMessage["MessageState"], -): { next: T[]; updated: boolean } { - let updated = false; - - const next = messages.map((item) => { - if (item.SendTime !== sendTime || item.MessageState === messageState) { - return item; - } - - updated = true; - return { - ...item, - MessageState: messageState, - }; - }); - - return { - next, - updated, - }; -} - -function updateMessageContentBySendTime< - T extends { SendTime: number; Content: string; Edited?: boolean }, ->( - messages: T[], - sendTime: number, - content: string, - edited = true, + edit: MessageEdit, ): { next: T[]; updated: boolean } { let updated = false; @@ -101,15 +76,18 @@ function updateMessageContentBySendTime< return item; } - if (item.Content === content && item.Edited === edited) { + const entries = Object.entries(edit) as Array< + [keyof MessageEdit, MessageEdit[keyof MessageEdit]] + >; + + if (entries.every(([key, value]) => item[key] === value)) { return item; } updated = true; return { ...item, - Content: content, - Edited: edited, + ...edit, }; }); @@ -408,52 +386,14 @@ export default function Provider({ children }: { children: ReactNode }) { useEffect(() => { load("user_id").then(setOwnId); }, [load]); - const addLiveMessage = useCallback( - (message: RawMessage) => { - const localId = - globalThis.crypto?.randomUUID?.() ?? - `${Date.now()}-${Math.random().toString(36).slice(2)}`; - if (message.SenderId !== ownId) { - moveUserIdToTop(userIdValue); - } - - setLiveMessagesState((prev) => [ - ...prev, - { - ...message, - localId, - failed: false, - }, - ]); - - return { - setFailed: (failed: boolean) => { - setLiveMessagesState((prev) => - prev.map((liveMessage) => - liveMessage.localId === localId - ? { ...liveMessage, failed } - : liveMessage, - ), - ); - }, - }; - }, - [userIdValue, moveUserIdToTop, ownId], - ); - - const clearLiveMessages = useCallback(() => { - setLiveMessagesState([]); - }, []); - - const editMessageContent = useCallback( - (sendTime: number, content: string, edited = true) => { + const editMessage = useCallback( + (sendTime: number, edit: MessageEdit) => { setLiveMessagesState((prev) => { - const { next, updated } = updateMessageContentBySendTime( + const { next, updated } = updateMessagesBySendTime( prev, sendTime, - content, - edited, + edit, ); return updated ? next : prev; }); @@ -473,12 +413,7 @@ export default function Provider({ children }: { children: ReactNode }) { let updated = false; const pages = current.pages.map((page) => { - const nextPage = updateMessageContentBySendTime( - page, - sendTime, - content, - edited, - ); + const nextPage = updateMessagesBySendTime(page, sendTime, edit); if (nextPage.updated) { updated = true; @@ -501,6 +436,38 @@ export default function Provider({ children }: { children: ReactNode }) { [currentChatSecret, userIdValue], ); + const addLiveMessage = useCallback( + (message: RawMessage) => { + const localId = + globalThis.crypto?.randomUUID?.() ?? + `${Date.now()}-${Math.random().toString(36).slice(2)}`; + + if (message.SenderId !== ownId) { + moveUserIdToTop(userIdValue); + } + + setLiveMessagesState((prev) => [ + ...prev, + { + ...message, + localId, + failed: false, + }, + ]); + + return { + setFailed: (failed: boolean) => { + editMessage(message.SendTime, { failed }); + }, + }; + }, + [editMessage, userIdValue, moveUserIdToTop, ownId], + ); + + const clearLiveMessages = useCallback(() => { + setLiveMessagesState([]); + }, []); + // Get live updates for message states useEffect(() => { return subscribePush((message) => { @@ -542,7 +509,7 @@ export default function Provider({ children }: { children: ReactNode }) { void decryptChatText(currentChatSecret, rawData.Content) .then((content) => { - editMessageContent(sendTime, content); + editMessage(sendTime, { Content: content, Edited: true }); }) .catch((err) => { log(1, "chat", "red", "Failed to decrypt message edit", err, { @@ -593,62 +560,11 @@ export default function Provider({ children }: { children: ReactNode }) { return; } - setLiveMessagesState((prev) => { - const { next, updated } = updateMessageStateBySendTime( - prev, - nextState.SendTime, - nextState.MessageState, - ); - return updated ? next : prev; + editMessage(nextState.SendTime, { + MessageState: nextState.MessageState, }); - - const queryKey = [ - "chat-messages", - String(userIdValue), - currentChatSecret !== null, - ] as const; - queryClient.setQueryData>( - queryKey, - (current) => { - if (!current) { - return current; - } - - let updated = false; - - const pages = current.pages.map((page) => { - const nextPage = updateMessageStateBySendTime( - page, - nextState.SendTime, - nextState.MessageState, - ); - - if (nextPage.updated) { - updated = true; - } - - return nextPage.next; - }); - - if (!updated) { - return current; - } - - return { - ...current, - pages, - }; - }, - ); }); - }, [ - addLiveMessage, - currentChatSecret, - editMessageContent, - send, - subscribePush, - userIdValue, - ]); + }, [currentChatSecret, editMessage, subscribePush, userIdValue]); // Replys const [replyTo, setReplyTo] = useState(undefined); @@ -661,7 +577,7 @@ export default function Provider({ children }: { children: ReactNode }) { getChatSecret, liveMessages: () => liveMessagesState, addLiveMessage, - editMessageContent, + editMessage, clearLiveMessages, chatSecret: currentChatSecret, userId: userIdValue, @@ -685,11 +601,7 @@ type contextType = { addLiveMessage: (message: RawMessage) => { setFailed: (failed: boolean) => void; }; - editMessageContent: ( - sendTime: number, - content: string, - edited?: boolean, - ) => void; + editMessage: (sendTime: number, edit: MessageEdit) => void; clearLiveMessages: () => void; chatSecret: Uint8Array | null; userId: number;