(feat): add message editing
Some checks failed
/ build-web (push) Successful in 6m58s
/ release (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-mobile (push) Has been cancelled

(qol): update todos
This commit is contained in:
Alois 2026-07-07 18:13:43 +02:00
commit 8dd73568ce
8 changed files with 310 additions and 6 deletions

View file

@ -86,6 +86,39 @@ function updateMessageStateBySendTime<
};
}
function updateMessageContentBySendTime<
T extends { SendTime: number; Content: string; Edited?: boolean },
>(
messages: T[],
sendTime: number,
content: string,
edited = true,
): { next: T[]; updated: boolean } {
let updated = false;
const next = messages.map((item) => {
if (item.SendTime !== sendTime) {
return item;
}
if (item.Content === content && item.Edited === edited) {
return item;
}
updated = true;
return {
...item,
Content: content,
Edited: edited,
};
});
return {
next,
updated,
};
}
function assertProtocolSuccess(type: string, response: { type: string }) {
if (response.type.startsWith("Error")) {
throw new Error(`${type} failed: ${response.type}`);
@ -413,9 +446,112 @@ export default function Provider({ children }: { children: ReactNode }) {
setLiveMessagesState([]);
}, []);
const editMessageContent = useCallback(
(sendTime: number, content: string, edited = true) => {
setLiveMessagesState((prev) => {
const { next, updated } = updateMessageContentBySendTime(
prev,
sendTime,
content,
edited,
);
return updated ? next : prev;
});
const queryKey = [
"chat-messages",
String(userIdValue),
currentChatSecret !== null,
] as const;
queryClient.setQueryData<InfiniteData<RawMessages>>(
queryKey,
(current) => {
if (!current) {
return current;
}
let updated = false;
const pages = current.pages.map((page) => {
const nextPage = updateMessageContentBySendTime(
page,
sendTime,
content,
edited,
);
if (nextPage.updated) {
updated = true;
}
return nextPage.next;
});
if (!updated) {
return current;
}
return {
...current,
pages,
};
},
);
},
[currentChatSecret, userIdValue],
);
// Get live updates for message states
useEffect(() => {
return subscribePush((message) => {
if (message.type === "MessageEditLive") {
if (!currentChatSecret) return;
const rawData = message.data as {
ChatPartnerId: unknown;
SendTime: unknown;
Content: string;
};
const chatPartnerId = Number(rawData.ChatPartnerId);
const sendTime = Number(rawData.SendTime);
if (!Number.isFinite(chatPartnerId) || !Number.isFinite(sendTime)) {
log(
3,
"chat",
"yellow",
"Cancel message edit update due to invalid data",
);
return;
}
if (chatPartnerId !== userIdValue) {
log(
3,
"chat",
"yellow",
"Cancel message edit update due to user ID mismatch",
{
expected: userIdValue,
received: chatPartnerId,
},
);
return;
}
void decryptChatText(currentChatSecret, rawData.Content)
.then((content) => {
editMessageContent(sendTime, content);
})
.catch((err) => {
log(1, "chat", "red", "Failed to decrypt message edit", err, {
SendTime: sendTime,
});
});
return;
}
if (message.type !== "MessageState") return;
const rawData = message.data as {
@ -505,7 +641,14 @@ export default function Provider({ children }: { children: ReactNode }) {
},
);
});
}, [addLiveMessage, currentChatSecret, send, subscribePush, userIdValue]);
}, [
addLiveMessage,
currentChatSecret,
editMessageContent,
send,
subscribePush,
userIdValue,
]);
// Replys
const [replyTo, setReplyTo] = useState<number | undefined>(undefined);
@ -518,6 +661,7 @@ export default function Provider({ children }: { children: ReactNode }) {
getChatSecret,
liveMessages: () => liveMessagesState,
addLiveMessage,
editMessageContent,
clearLiveMessages,
chatSecret: currentChatSecret,
userId: userIdValue,
@ -541,6 +685,11 @@ type contextType = {
addLiveMessage: (message: RawMessage) => {
setFailed: (failed: boolean) => void;
};
editMessageContent: (
sendTime: number,
content: string,
edited?: boolean,
) => void;
clearLiveMessages: () => void;
chatSecret: Uint8Array | null;
userId: number;