(feat): unify message patching into a single interface
This commit is contained in:
parent
8dd73568ce
commit
a08c41067e
2 changed files with 64 additions and 153 deletions
|
|
@ -143,7 +143,7 @@ function MessageComponent({
|
||||||
}, [message.Content]);
|
}, [message.Content]);
|
||||||
|
|
||||||
// Message editing
|
// Message editing
|
||||||
const { chatSecret, editMessageContent, userId } = useChat();
|
const { chatSecret, editMessage, userId } = useChat();
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [editDraft, setEditDraft] = useState(message.Content);
|
const [editDraft, setEditDraft] = useState(message.Content);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -151,7 +151,7 @@ function MessageComponent({
|
||||||
setEditDraft(message.Content);
|
setEditDraft(message.Content);
|
||||||
}
|
}
|
||||||
}, [editing, message.Content]);
|
}, [editing, message.Content]);
|
||||||
const editMessage = useCallback(
|
const submitEditMessage = useCallback(
|
||||||
async (newContent: string) => {
|
async (newContent: string) => {
|
||||||
if (!chatSecret) return;
|
if (!chatSecret) return;
|
||||||
|
|
||||||
|
|
@ -165,7 +165,7 @@ function MessageComponent({
|
||||||
if (!encryptedContent) return;
|
if (!encryptedContent) return;
|
||||||
|
|
||||||
const previousContent = message.Content;
|
const previousContent = message.Content;
|
||||||
editMessageContent(message.SendTime, newContent);
|
editMessage(message.SendTime, { Content: newContent, Edited: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await send("MessageEdit", {
|
const response = await send("MessageEdit", {
|
||||||
|
|
@ -178,18 +178,17 @@ function MessageComponent({
|
||||||
throw new Error(response.type);
|
throw new Error(response.type);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
editMessageContent(
|
editMessage(message.SendTime, {
|
||||||
message.SendTime,
|
Content: previousContent,
|
||||||
previousContent,
|
Edited: message.Edited ?? false,
|
||||||
message.Edited ?? false,
|
});
|
||||||
);
|
|
||||||
log(1, "chat", "red", "Failed to edit message", err);
|
log(1, "chat", "red", "Failed to edit message", err);
|
||||||
toast("error", "Failed to edit message", String(err));
|
toast("error", "Failed to edit message", String(err));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
chatSecret,
|
chatSecret,
|
||||||
editMessageContent,
|
editMessage,
|
||||||
userId,
|
userId,
|
||||||
message.Content,
|
message.Content,
|
||||||
message.Edited,
|
message.Edited,
|
||||||
|
|
@ -288,7 +287,7 @@ function MessageComponent({
|
||||||
setValue={setEditDraft}
|
setValue={setEditDraft}
|
||||||
value={editDraft}
|
value={editDraft}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
editMessage(editDraft);
|
submitEditMessage(editDraft);
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -59,40 +59,15 @@ function bytesFromProtocol(value: unknown): Uint8Array {
|
||||||
throw new Error("expected protocol bytes");
|
throw new Error("expected protocol bytes");
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMessageStateBySendTime<
|
type EditableMessage = RawMessage & { failed?: boolean };
|
||||||
T extends { SendTime: number; MessageState: RawMessage["MessageState"] },
|
type MessageEdit = Partial<
|
||||||
>(
|
Pick<EditableMessage, "Content" | "Edited" | "MessageState" | "failed">
|
||||||
|
>;
|
||||||
|
|
||||||
|
function updateMessagesBySendTime<T extends EditableMessage>(
|
||||||
messages: T[],
|
messages: T[],
|
||||||
sendTime: number,
|
sendTime: number,
|
||||||
messageState: RawMessage["MessageState"],
|
edit: MessageEdit,
|
||||||
): { 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,
|
|
||||||
): { next: T[]; updated: boolean } {
|
): { next: T[]; updated: boolean } {
|
||||||
let updated = false;
|
let updated = false;
|
||||||
|
|
||||||
|
|
@ -101,15 +76,18 @@ function updateMessageContentBySendTime<
|
||||||
return item;
|
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;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
updated = true;
|
updated = true;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
Content: content,
|
...edit,
|
||||||
Edited: edited,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -408,52 +386,14 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load("user_id").then(setOwnId);
|
load("user_id").then(setOwnId);
|
||||||
}, [load]);
|
}, [load]);
|
||||||
const addLiveMessage = useCallback(
|
|
||||||
(message: RawMessage) => {
|
|
||||||
const localId =
|
|
||||||
globalThis.crypto?.randomUUID?.() ??
|
|
||||||
`${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
||||||
|
|
||||||
if (message.SenderId !== ownId) {
|
const editMessage = useCallback(
|
||||||
moveUserIdToTop(userIdValue);
|
(sendTime: number, edit: MessageEdit) => {
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
setLiveMessagesState((prev) => {
|
setLiveMessagesState((prev) => {
|
||||||
const { next, updated } = updateMessageContentBySendTime(
|
const { next, updated } = updateMessagesBySendTime(
|
||||||
prev,
|
prev,
|
||||||
sendTime,
|
sendTime,
|
||||||
content,
|
edit,
|
||||||
edited,
|
|
||||||
);
|
);
|
||||||
return updated ? next : prev;
|
return updated ? next : prev;
|
||||||
});
|
});
|
||||||
|
|
@ -473,12 +413,7 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
let updated = false;
|
let updated = false;
|
||||||
|
|
||||||
const pages = current.pages.map((page) => {
|
const pages = current.pages.map((page) => {
|
||||||
const nextPage = updateMessageContentBySendTime(
|
const nextPage = updateMessagesBySendTime(page, sendTime, edit);
|
||||||
page,
|
|
||||||
sendTime,
|
|
||||||
content,
|
|
||||||
edited,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextPage.updated) {
|
if (nextPage.updated) {
|
||||||
updated = true;
|
updated = true;
|
||||||
|
|
@ -501,6 +436,38 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
[currentChatSecret, userIdValue],
|
[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
|
// Get live updates for message states
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return subscribePush((message) => {
|
return subscribePush((message) => {
|
||||||
|
|
@ -542,7 +509,7 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
void decryptChatText(currentChatSecret, rawData.Content)
|
void decryptChatText(currentChatSecret, rawData.Content)
|
||||||
.then((content) => {
|
.then((content) => {
|
||||||
editMessageContent(sendTime, content);
|
editMessage(sendTime, { Content: content, Edited: true });
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
log(1, "chat", "red", "Failed to decrypt message edit", err, {
|
log(1, "chat", "red", "Failed to decrypt message edit", err, {
|
||||||
|
|
@ -593,62 +560,11 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLiveMessagesState((prev) => {
|
editMessage(nextState.SendTime, {
|
||||||
const { next, updated } = updateMessageStateBySendTime(
|
MessageState: nextState.MessageState,
|
||||||
prev,
|
|
||||||
nextState.SendTime,
|
|
||||||
nextState.MessageState,
|
|
||||||
);
|
|
||||||
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 = updateMessageStateBySendTime(
|
|
||||||
page,
|
|
||||||
nextState.SendTime,
|
|
||||||
nextState.MessageState,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextPage.updated) {
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextPage.next;
|
|
||||||
});
|
});
|
||||||
|
}, [currentChatSecret, editMessage, subscribePush, userIdValue]);
|
||||||
if (!updated) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
pages,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
addLiveMessage,
|
|
||||||
currentChatSecret,
|
|
||||||
editMessageContent,
|
|
||||||
send,
|
|
||||||
subscribePush,
|
|
||||||
userIdValue,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Replys
|
// Replys
|
||||||
const [replyTo, setReplyTo] = useState<number | undefined>(undefined);
|
const [replyTo, setReplyTo] = useState<number | undefined>(undefined);
|
||||||
|
|
@ -661,7 +577,7 @@ export default function Provider({ children }: { children: ReactNode }) {
|
||||||
getChatSecret,
|
getChatSecret,
|
||||||
liveMessages: () => liveMessagesState,
|
liveMessages: () => liveMessagesState,
|
||||||
addLiveMessage,
|
addLiveMessage,
|
||||||
editMessageContent,
|
editMessage,
|
||||||
clearLiveMessages,
|
clearLiveMessages,
|
||||||
chatSecret: currentChatSecret,
|
chatSecret: currentChatSecret,
|
||||||
userId: userIdValue,
|
userId: userIdValue,
|
||||||
|
|
@ -685,11 +601,7 @@ type contextType = {
|
||||||
addLiveMessage: (message: RawMessage) => {
|
addLiveMessage: (message: RawMessage) => {
|
||||||
setFailed: (failed: boolean) => void;
|
setFailed: (failed: boolean) => void;
|
||||||
};
|
};
|
||||||
editMessageContent: (
|
editMessage: (sendTime: number, edit: MessageEdit) => void;
|
||||||
sendTime: number,
|
|
||||||
content: string,
|
|
||||||
edited?: boolean,
|
|
||||||
) => void;
|
|
||||||
clearLiveMessages: () => void;
|
clearLiveMessages: () => void;
|
||||||
chatSecret: Uint8Array | null;
|
chatSecret: Uint8Array | null;
|
||||||
userId: number;
|
userId: number;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue