Compare commits

..
Author SHA1 Message Date
bbf9b193d8 chore(deps): update rust crate base64 to 0.23
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Dependency builds / Build web (pull_request) Failing after 2m8s
Dependency builds / Build desktop (pull_request) Failing after 2m15s
Dependency builds / Test native MTP (pull_request) Failing after 3m53s
Dependency builds / Build mobile (pull_request) Failing after 4m43s
2026-08-30 23:00:39 +03:00
Alex Emmet
6e31989262
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
Some checks failed
/ build-web (push) Failing after 2m6s
/ build-desktop (linux) (push) Failing after 2m10s
/ build-mobile (push) Failing after 3m46s
/ release (push) Has been skipped
2026-08-30 21:28:51 +02:00
Alex Emmet
c155bbb534
[Fix] Durability 2026-08-30 21:28:36 +02:00
4 changed files with 61 additions and 38 deletions

View file

@ -15,7 +15,7 @@ import {
import MessageContextMenu from "./messageContextMenu"; import MessageContextMenu from "./messageContextMenu";
import Media from "./media/media"; import Media from "./media/media";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { useMTP } from "@tensamin/mtp"; import { requireRelaySuccess, useMTP } from "@tensamin/mtp";
import { getMessage, useChat } from "../context"; import { getMessage, useChat } from "../context";
import { decryptChatText, encryptChatText } from "@tensamin/crypto/chatSecret"; import { decryptChatText, encryptChatText } from "@tensamin/crypto/chatSecret";
import { log, toast } from "@tensamin/shared/log"; import { log, toast } from "@tensamin/shared/log";
@ -42,6 +42,7 @@ function MessageComponent({
user: SelectedUser<readonly ["UserId", "Avatar", "Display"]> | null; user: SelectedUser<readonly ["UserId", "Avatar", "Display"]> | null;
}) { }) {
const actuallyFailed = const actuallyFailed =
Boolean(message.ErrorType) ||
(message.failed && message.MessageState === "awaiting") || (message.failed && message.MessageState === "awaiting") ||
message.decryptionFailed; message.decryptionFailed;
@ -65,7 +66,7 @@ function MessageComponent({
// Message states // Message states
const { load } = useStorage(); const { load } = useStorage();
const { send } = useMTP(); const { send, sendSealedRelay } = useMTP();
const [ownId, setOwnId] = useState(0); const [ownId, setOwnId] = useState(0);
useEffect(() => { useEffect(() => {
load("user_id").then(setOwnId); load("user_id").then(setOwnId);
@ -73,22 +74,38 @@ function MessageComponent({
const messageStateReadUpdate = useCallback(async () => { const messageStateReadUpdate = useCallback(async () => {
const readConfirmations = await load("settings.read_confirmations"); const readConfirmations = await load("settings.read_confirmations");
if (readConfirmations) { if (!user?.UserId || !message.RelayMessageId || ownId === 0) return;
if (!user?.UserId) return; const [ownIota, peerIota] = await Promise.all([
send("GetIotaData", { UserId: ownId }),
await send("MessageState", { send("GetIotaData", { UserId: user.UserId }),
ChatPartnerId: user?.UserId, ]);
SendTime: message.SendTime, if (ownIota.type !== "GetIotaData" || peerIota.type !== "GetIotaData") {
MessageState: "read", throw new Error("Could not resolve an Iota for this receipt");
});
} else {
await send("MessageState", {
ChatPartnerId: user?.UserId,
SendTime: message.SendTime,
MessageState: "received",
});
} }
}, [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(() => { useEffect(() => {
if (message.SenderId === ownId || ownId === 0) return; if (message.SenderId === ownId || ownId === 0) return;

View file

@ -69,6 +69,14 @@ type MessageEdit = Partial<
> >
>; >;
const messageStateRank: Record<RawMessage["MessageState"], number> = {
awaiting: 0,
sending: 0,
sent: 1,
received: 2,
read: 3,
};
function updateMessagesBySendTime<T extends EditableMessage>( function updateMessagesBySendTime<T extends EditableMessage>(
messages: T[], messages: T[],
sendTime: number, sendTime: number,
@ -81,6 +89,13 @@ function updateMessagesBySendTime<T extends EditableMessage>(
return item; return item;
} }
if (
edit.MessageState !== undefined &&
messageStateRank[edit.MessageState] < messageStateRank[item.MessageState]
) {
return item;
}
const entries = Object.entries(edit) as Array< const entries = Object.entries(edit) as Array<
[keyof MessageEdit, MessageEdit[keyof MessageEdit]] [keyof MessageEdit, MessageEdit[keyof MessageEdit]]
>; >;
@ -983,9 +998,7 @@ export default function Provider({ children }: { children: ReactNode }) {
); );
return; return;
} }
editMessage(data.SendTime, { editMessage(data.SendTime, { MessageState: data.MessageState });
MessageState: data.MessageState,
});
}); });
return () => { return () => {
unsubscribeEdit(); unsubscribeEdit();

View file

@ -29,7 +29,7 @@ async function requestNotificationPermission() {
} }
export default function Provider(props: { children: React.ReactNode }) { export default function Provider(props: { children: React.ReactNode }) {
const { subscribe, send } = useMTP(); const { subscribe } = useMTP();
const { load } = useStorage(); const { load } = useStorage();
const { get } = useUser(); const { get } = useUser();
const { addLiveMessage, chatSecret, getChatSecret, userId } = useChat(); 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) // todo: add notification symbol to conversation cards (incl. message start)
moveUserIdToTop(data.SenderId); moveUserIdToTop(data.SenderId);
if (await load("settings.receive_confirmations")) {
void send("MessageState", {
MessageState: "received",
});
}
} }
const user = await get(data.SenderId, [ const user = await get(data.SenderId, [
@ -180,7 +175,6 @@ export default function Provider(props: { children: React.ReactNode }) {
load, load,
location.pathname, location.pathname,
navigate, navigate,
send,
moveUserIdToTop, moveUserIdToTop,
subscribe, subscribe,
getChatSecret, getChatSecret,

View file

@ -70,6 +70,7 @@ const callSecretEnvelopeRequest = z.object({
export const Reaction = z.object({ export const Reaction = z.object({
Reaction: z.string(), Reaction: z.string(),
SenderId: z.number(), SenderId: z.number(),
RelayMessageId: z.string().optional(),
}); });
export const Message = z.object({ export const Message = z.object({
@ -82,6 +83,8 @@ export const Message = z.object({
Avatar: z.boolean().optional(), Avatar: z.boolean().optional(),
Display: z.boolean().optional(), Display: z.boolean().optional(),
ReplyId: z.number().optional(), ReplyId: z.number().optional(),
UpdatedAt: z.number().optional(),
ErrorType: z.string().optional(),
MessageState: z MessageState: z
.enum(["read", "received", "sent", "sending", "awaiting"]) // awaiting for 'internal' use .enum(["read", "received", "sent", "sending", "awaiting"]) // awaiting for 'internal' use
.default("received"), .default("received"),
@ -107,6 +110,7 @@ const authPayload = z.object({
.array( .array(
z.object({ z.object({
LastMessageAt: z.number().default(0), LastMessageAt: z.number().default(0),
CreatedAt: z.number().optional(),
UserId: z.number(), UserId: z.number(),
LastMessage: z LastMessage: z
.object({ .object({
@ -397,17 +401,12 @@ export const mtp = {
response: z.object({}), response: z.object({}),
}, },
MessageState: { MessageState: {
request: z request: z.object({
.object({
ChatPartnerId: z.number(), ChatPartnerId: z.number(),
SendTime: z.number(), RelayMessageId: z.string(),
MessageState: Message.shape.MessageState, EventAt: z.number(),
}) MessageState: z.enum(["received", "read"]),
.or(
z.object({
MessageState: Message.shape.MessageState,
}), }),
),
response: z.object({ response: z.object({
ChatPartnerId: z.number(), ChatPartnerId: z.number(),
MessageState: Message.shape.MessageState, MessageState: Message.shape.MessageState,