From c155bbb5340dc7dae8f32f44d5795ced010c7e97 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:28:36 +0200 Subject: [PATCH 1/2] [Fix] Durability --- packages/chat/src/components/message.tsx | 51 ++++++++++++++++-------- packages/chat/src/context.tsx | 19 +++++++-- packages/notifications/src/context.tsx | 8 +--- packages/shared/src/data.ts | 21 +++++----- 4 files changed, 61 insertions(+), 38 deletions(-) diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index cbcd37e..a2b153c 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -15,7 +15,7 @@ import { import MessageContextMenu from "./messageContextMenu"; import Media from "./media/media"; import { useStorage } from "@tensamin/storage/context"; -import { useMTP } from "@tensamin/mtp"; +import { requireRelaySuccess, useMTP } from "@tensamin/mtp"; import { getMessage, useChat } from "../context"; import { decryptChatText, encryptChatText } from "@tensamin/crypto/chatSecret"; import { log, toast } from "@tensamin/shared/log"; @@ -42,6 +42,7 @@ function MessageComponent({ user: SelectedUser | null; }) { const actuallyFailed = + Boolean(message.ErrorType) || (message.failed && message.MessageState === "awaiting") || message.decryptionFailed; @@ -65,7 +66,7 @@ function MessageComponent({ // Message states const { load } = useStorage(); - const { send } = useMTP(); + const { send, sendSealedRelay } = useMTP(); const [ownId, setOwnId] = useState(0); useEffect(() => { load("user_id").then(setOwnId); @@ -73,22 +74,38 @@ function MessageComponent({ const messageStateReadUpdate = useCallback(async () => { const readConfirmations = await load("settings.read_confirmations"); - if (readConfirmations) { - if (!user?.UserId) return; - - await send("MessageState", { - ChatPartnerId: user?.UserId, - SendTime: message.SendTime, - MessageState: "read", - }); - } else { - await send("MessageState", { - ChatPartnerId: user?.UserId, - SendTime: message.SendTime, - MessageState: "received", - }); + if (!user?.UserId || !message.RelayMessageId || ownId === 0) return; + const [ownIota, peerIota] = await Promise.all([ + send("GetIotaData", { UserId: ownId }), + send("GetIotaData", { UserId: user.UserId }), + ]); + if (ownIota.type !== "GetIotaData" || peerIota.type !== "GetIotaData") { + throw new Error("Could not resolve an Iota for this receipt"); } - }, [load, message.SendTime, user?.UserId, send]); + const response = await sendSealedRelay( + "MessageState", + { + ChatPartnerId: user.UserId, + RelayMessageId: message.RelayMessageId, + EventAt: Date.now(), + MessageState: readConfirmations ? "read" : "received", + ReceiverId: user.UserId, + }, + { + nextHop: { kind: "iota", id: ownIota.data.IotaId }, + finalRecipientId: user.UserId, + metadataRecipients: [ + { value: ownIota.data.PublicKey, encoding: "base64" }, + { value: peerIota.data.PublicKey, encoding: "base64" }, + ], + contentRecipients: [ + { value: ownIota.data.PublicKey, encoding: "base64" }, + { value: peerIota.data.PublicKey, encoding: "base64" }, + ], + }, + ); + requireRelaySuccess(response); + }, [load, message.RelayMessageId, ownId, send, sendSealedRelay, user]); useEffect(() => { if (message.SenderId === ownId || ownId === 0) return; diff --git a/packages/chat/src/context.tsx b/packages/chat/src/context.tsx index 5cf3535..d307a12 100644 --- a/packages/chat/src/context.tsx +++ b/packages/chat/src/context.tsx @@ -69,6 +69,14 @@ type MessageEdit = Partial< > >; +const messageStateRank: Record = { + awaiting: 0, + sending: 0, + sent: 1, + received: 2, + read: 3, +}; + function updateMessagesBySendTime( messages: T[], sendTime: number, @@ -81,6 +89,13 @@ function updateMessagesBySendTime( return item; } + if ( + edit.MessageState !== undefined && + messageStateRank[edit.MessageState] < messageStateRank[item.MessageState] + ) { + return item; + } + const entries = Object.entries(edit) as Array< [keyof MessageEdit, MessageEdit[keyof MessageEdit]] >; @@ -989,9 +1004,7 @@ export default function Provider({ children }: { children: ReactNode }) { ); return; } - editMessage(data.SendTime, { - MessageState: data.MessageState, - }); + editMessage(data.SendTime, { MessageState: data.MessageState }); }); return () => { unsubscribeEdit(); diff --git a/packages/notifications/src/context.tsx b/packages/notifications/src/context.tsx index e0da277..ef80f33 100644 --- a/packages/notifications/src/context.tsx +++ b/packages/notifications/src/context.tsx @@ -29,7 +29,7 @@ async function requestNotificationPermission() { } export default function Provider(props: { children: React.ReactNode }) { - const { subscribe, send } = useMTP(); + const { subscribe } = useMTP(); const { load } = useStorage(); const { get } = useUser(); const { addLiveMessage, chatSecret, getChatSecret, userId } = useChat(); @@ -79,11 +79,6 @@ export default function Provider(props: { children: React.ReactNode }) { // todo: add notification symbol to conversation cards (incl. message start) moveUserIdToTop(data.SenderId); - if (await load("settings.receive_confirmations")) { - void send("MessageState", { - MessageState: "received", - }); - } } const user = await get(data.SenderId, [ @@ -180,7 +175,6 @@ export default function Provider(props: { children: React.ReactNode }) { load, location.pathname, navigate, - send, moveUserIdToTop, subscribe, getChatSecret, diff --git a/packages/shared/src/data.ts b/packages/shared/src/data.ts index 4a64c80..e7d2c33 100644 --- a/packages/shared/src/data.ts +++ b/packages/shared/src/data.ts @@ -70,6 +70,7 @@ const callSecretEnvelopeRequest = z.object({ export const Reaction = z.object({ Reaction: z.string(), SenderId: z.number(), + RelayMessageId: z.string().optional(), }); export const Message = z.object({ @@ -82,6 +83,8 @@ export const Message = z.object({ Avatar: z.boolean().optional(), Display: z.boolean().optional(), ReplyId: z.number().optional(), + UpdatedAt: z.number().optional(), + ErrorType: z.string().optional(), MessageState: z .enum(["read", "received", "sent", "sending", "awaiting"]) // awaiting for 'internal' use .default("received"), @@ -107,6 +110,7 @@ const authPayload = z.object({ .array( z.object({ LastMessageAt: z.number().default(0), + CreatedAt: z.number().optional(), UserId: z.number(), LastMessage: z .object({ @@ -397,17 +401,12 @@ export const mtp = { response: z.object({}), }, MessageState: { - request: z - .object({ - ChatPartnerId: z.number(), - SendTime: z.number(), - MessageState: Message.shape.MessageState, - }) - .or( - z.object({ - MessageState: Message.shape.MessageState, - }), - ), + request: z.object({ + ChatPartnerId: z.number(), + RelayMessageId: z.string(), + EventAt: z.number(), + MessageState: z.enum(["received", "read"]), + }), response: z.object({ ChatPartnerId: z.number(), MessageState: Message.shape.MessageState, From e4cdce77b755c8b8532bc93e8b30c230e731bb23 Mon Sep 17 00:00:00 2001 From: Rasensprenger Date: Sun, 30 Aug 2026 23:00:28 +0300 Subject: [PATCH 2/2] chore(deps): update mtp digest to f6a8b46 --- apps/tauri/src-tauri/Cargo.lock | 19 +++++++++---------- apps/tauri/src-tauri/Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index d404d61..822aa43 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -2745,7 +2745,7 @@ dependencies = [ [[package]] name = "mtp" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=f6a8b464e0daae6c7753208f3d8601d21cd47ab2#f6a8b464e0daae6c7753208f3d8601d21cd47ab2" dependencies = [ "mtp-client", "mtp-codec", @@ -2760,7 +2760,7 @@ dependencies = [ [[package]] name = "mtp-client" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=f6a8b464e0daae6c7753208f3d8601d21cd47ab2#f6a8b464e0daae6c7753208f3d8601d21cd47ab2" dependencies = [ "mtp-codec", "mtp-common", @@ -2773,7 +2773,7 @@ dependencies = [ [[package]] name = "mtp-codec" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=f6a8b464e0daae6c7753208f3d8601d21cd47ab2#f6a8b464e0daae6c7753208f3d8601d21cd47ab2" dependencies = [ "base64 0.23.1", "byteorder", @@ -2787,10 +2787,9 @@ dependencies = [ [[package]] name = "mtp-common" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=f6a8b464e0daae6c7753208f3d8601d21cd47ab2#f6a8b464e0daae6c7753208f3d8601d21cd47ab2" dependencies = [ "quinn", - "rustls", "thiserror 2.0.19", "wtransport", ] @@ -2798,7 +2797,7 @@ dependencies = [ [[package]] name = "mtp-crypto" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=f6a8b464e0daae6c7753208f3d8601d21cd47ab2#f6a8b464e0daae6c7753208f3d8601d21cd47ab2" dependencies = [ "base64 0.22.1", "chacha20poly1305", @@ -2820,7 +2819,7 @@ dependencies = [ [[package]] name = "mtp-host" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=f6a8b464e0daae6c7753208f3d8601d21cd47ab2#f6a8b464e0daae6c7753208f3d8601d21cd47ab2" dependencies = [ "mtp-codec", "mtp-common", @@ -2836,7 +2835,7 @@ dependencies = [ [[package]] name = "mtp-transport" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=f6a8b464e0daae6c7753208f3d8601d21cd47ab2#f6a8b464e0daae6c7753208f3d8601d21cd47ab2" dependencies = [ "async-trait", "mtp-codec", @@ -2856,7 +2855,7 @@ dependencies = [ [[package]] name = "mtp-type-map" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=f6a8b464e0daae6c7753208f3d8601d21cd47ab2#f6a8b464e0daae6c7753208f3d8601d21cd47ab2" dependencies = [ "serde", "serde_yaml", @@ -2865,7 +2864,7 @@ dependencies = [ [[package]] name = "mtp-webserver" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git?rev=f6a8b464e0daae6c7753208f3d8601d21cd47ab2#f6a8b464e0daae6c7753208f3d8601d21cd47ab2" dependencies = [ "async-trait", "bytes", diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 2a68e95..fd27d12 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -24,7 +24,7 @@ serde_json = "1" base64 = "0.22" reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } -mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a5c8d4f0c898c78351e9d54124886c86e789a22a", features = ["client", "crypto"] } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "f6a8b464e0daae6c7753208f3d8601d21cd47ab2", features = ["client", "crypto"] } webpki-root-certs = "1" tauri-plugin-deep-link = "2" tauri-plugin-notification = "2"