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,