(feat): migrate ttp to mtp
Some checks failed
/ build-desktop (linux) (push) Failing after 3m58s
/ build-web (push) Failing after 4m13s
/ build-mobile (push) Failing after 6m34s
/ release (push) Has been skipped

(wip): crypto migration
This commit is contained in:
Alois 2026-07-05 01:43:06 +02:00
commit 930663d495
30 changed files with 559 additions and 749 deletions

View file

@ -11,13 +11,13 @@ import type { RawMessages } from "../values";
*/
export async function getMessages(
send: BoundSendFn,
amount: number,
offset: number,
Amount: number,
Offset: number,
UserId: number,
): Promise<RawMessages> {
const messages = await send("messages_get", {
amount: amount,
offset: offset,
const messages = await send("MessagesGet", {
Amount,
Offset,
UserId,
});
@ -25,5 +25,5 @@ export async function getMessages(
throw new Error(messages.type);
}
return messages.data.messages;
return messages.data.Messages;
}

View file

@ -72,10 +72,9 @@ export default function InputComponent({
log(3, "chat", "purple", "Message send init, adding live message ...");
const reference = addLiveMessage({
height: 0,
NotEncrypted: true,
SendTime: time,
content: currentValue,
Content: currentValue,
SentBySelf: true,
MessageState: "awaiting",
});
@ -94,9 +93,8 @@ export default function InputComponent({
log(3, "chat", "purple", "Content encrypted, sending message...");
send("message_send", {
height: 0,
content: encryptedContext,
send("MessageSend", {
Content: encryptedContext,
ReceiverId: userId,
SendTime: time,
}).catch((e) => {

View file

@ -64,14 +64,14 @@ function MessageComponent({
const [isValidURL, setIsValidURL] = useState(false);
useEffect(() => {
try {
if (message.content.split(" ").length > 1) throw new Error();
if (message.Content.split(" ").length > 1) throw new Error();
new URL(message.content);
new URL(message.Content);
setIsValidURL(true);
} catch {
setIsValidURL(false);
}
}, [message.content]);
}, [message.Content]);
// Message states
const { load } = useStorage();
@ -82,13 +82,13 @@ function MessageComponent({
if (readConfirmations) {
if (!user?.UserId) return;
await send("message_state", {
await send("MessageState", {
ChatPartnerId: user?.UserId,
SendTime: message.SendTime,
MessageState: "read",
});
} else {
await send("message_state", {
await send("MessageState", {
ChatPartnerId: user?.UserId,
SendTime: message.SendTime,
MessageState: "received",
@ -131,7 +131,7 @@ function MessageComponent({
className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${opacityClass}`}
>
<MessageContextMenu
content={message.content}
content={message.Content}
messageId={message.SendTime}
>
<div
@ -154,9 +154,9 @@ function MessageComponent({
</p>
) : (
<Avatar className="mr-1 mb-auto mt-1 w-10">
<AvatarImage src={user.avatar} />
<AvatarImage src={user.Avatar} />
<AvatarFallback>
{user.display.slice(0, 2).toUpperCase()}
{user.Display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
)}
@ -190,7 +190,7 @@ function MessageComponent({
</Card>
{!grouped && (
<div className="flex items-center gap-1">
<p className="font-medium">{user.display}</p>
<p className="font-medium">{user.Display}</p>
<p className="text-xs text-muted-foreground">
{new Date(message.SendTime).toLocaleString([], {
hour: "2-digit",
@ -214,9 +214,9 @@ function MessageComponent({
</div>
)}
{isValidURL ? (
<Media link={message.content} />
<Media link={message.Content} />
) : (
<Text value={message.content} />
<Text value={message.Content} />
)}
</div>
</>
@ -232,8 +232,7 @@ function MessageComponent({
export default React.memo(MessageComponent, (prev, next) => {
return (
prev.message.SendTime === next.message.SendTime &&
prev.message.content === next.message.content &&
prev.message.height === next.message.height &&
prev.message.Content === next.message.Content &&
prev.message.SentBySelf === next.message.SentBySelf &&
prev.message.MessageState === next.message.MessageState &&
prev.message.failed === next.message.failed &&

View file

@ -50,18 +50,16 @@ function updateMessageStateBySendTime<
};
}
/**
* Executes Provider.
* @param props Parameter props.
* @returns unknown.
*/
export default function Provider(props: { children: ReactNode }) {
export default function Provider({ children }: { children: ReactNode }) {
const { getSharedSecret, decryptText } = useCrypto();
const { get } = useUser();
const { load } = useStorage();
const { send, subscribePush } = useMTP();
const { moveUserIdToTop } = useSession();
const [error, setError] = useState("");
const [errorDescription, setErrorDescription] = useState("");
const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
const [currentSharedSecretState, setCurrentSharedSecretState] = useState<{
userId: number;
@ -101,21 +99,40 @@ export default function Provider(props: { children: ReactNode }) {
try {
const recipientData = await get(userIdValue);
const ownId = await load("user_id");
const privateKey = await load("private_key");
const privateKey = await load("mtp_keyring");
const ownData = await get(ownId);
log(3, "chat", "purple", "Getting shared secret...", {
recipientData,
ownData,
});
const sharedSecret = await getSharedSecret(
privateKey,
ownData.PublicKey,
recipientData.PublicKey,
);
log(2, "chat", "purple", "Got shared secret", {
sharedSecret,
});
if (active) {
setCurrentSharedSecretState({
userId: userIdValue,
value: sharedSecret,
});
}
} catch {
} catch (err) {
log(
1,
"chat",
"red",
"An unknown error occured while getting a shared secret",
err,
);
setError(err instanceof Error ? err.name : "Unknown Error");
setErrorDescription(err instanceof Error ? err.message : String(err));
if (active) {
setCurrentSharedSecretState({
userId: userIdValue,
@ -132,9 +149,9 @@ export default function Provider(props: { children: ReactNode }) {
const getMessages = useCallback(
async (amount: number, offset: number) => {
const messages = await send("messages_get", {
amount,
offset,
const messages = await send("MessagesGet", {
Amount: amount,
Offset: offset,
UserId: userIdValue,
});
@ -142,7 +159,7 @@ export default function Provider(props: { children: ReactNode }) {
throw new Error(messages.type);
}
const rawMessages = messages.data.messages;
const rawMessages = messages.data.Messages;
const sorted = [...rawMessages].sort((a, b) => a.SendTime - b.SendTime);
if (sorted.length > 0) {
@ -162,7 +179,7 @@ export default function Provider(props: { children: ReactNode }) {
try {
return {
...message,
content: await decryptText(currentSharedSecret, message.content),
content: await decryptText(currentSharedSecret, message.Content),
};
} catch {
return message;
@ -214,7 +231,7 @@ export default function Provider(props: { children: ReactNode }) {
// Get live updates for message states
useEffect(() => {
return subscribePush((message) => {
if (message.type !== "message_state") {
if (message.type !== "MessageState") {
return;
}
@ -318,9 +335,11 @@ export default function Provider(props: { children: ReactNode }) {
sharedSecret: currentSharedSecret,
userId: userIdValue,
inputBoxRef,
error,
errorDescription,
}}
>
{props.children}
{children}
</context.Provider>
</QueryClientProvider>
);
@ -336,13 +355,10 @@ type contextType = {
sharedSecret: string;
userId: number;
inputBoxRef: React.RefObject<HTMLDivElement | null>;
error: string;
errorDescription: string;
};
/**
* Executes useChat.
* @param none This function has no parameters.
* @returns contextType.
*/
export function useChat(): contextType {
const ctx = useContext(context);
if (!ctx) {

View file

@ -22,12 +22,6 @@ type MessageChunk = {
startIndex: number;
};
function getEstimatedMessageHeight(message: { height?: number }) {
return typeof message.height === "number" && Number.isFinite(message.height)
? Math.max(1, Math.ceil(message.height))
: FALLBACK_MESSAGE_HEIGHT;
}
function shouldFetchPreviousPage({
entry,
hasNextPage,
@ -80,8 +74,15 @@ function buildMessageChunks(
* @returns Chat screen JSX.
*/
export default function Screen() {
const { getMessages, liveMessages, clearLiveMessages, userId, sharedSecret } =
useChat();
const {
getMessages,
liveMessages,
clearLiveMessages,
userId,
sharedSecret,
error,
errorDescription,
} = useChat();
const { get: getUser } = useUser();
const { load } = useStorage();
@ -251,16 +252,7 @@ export default function Screen() {
return FALLBACK_MESSAGE_HEIGHT;
}
const chunk = messageChunks[index];
if (!chunk) {
return FALLBACK_MESSAGE_HEIGHT;
}
return chunk.messages.reduce(
(total, message) => total + getEstimatedMessageHeight(message),
0,
);
return FALLBACK_MESSAGE_HEIGHT;
},
[messageChunks, shouldShowConversationStart],
);
@ -479,112 +471,122 @@ export default function Screen() {
if (!hasValidChatUser) {
return (
<div className="w-full h-full flex items-center justify-center text-xl text-foreground/80">
Invalid user
<div className="w-full h-full flex flex-col gap-2 items-center justify-center">
<p className="font-semibold text-xl">Invalid User</p>
</div>
);
}
return (
<div className="relative flex h-full min-h-0 w-full flex-col overflow-hidden">
<div
ref={scrollRef}
id="chat_container"
className="min-h-0 flex-1 overflow-y-auto"
style={{
overflowAnchor: "none",
paddingTop: "22px",
transform: "scaleY(-1)",
}}
onScroll={handleContainerScroll}
>
<div
className="relative w-full"
style={{ height: `${contentHeight}px` }}
>
{error !== "" && errorDescription !== "" ? (
<div className="w-full h-full flex flex-col gap-2 items-center justify-center">
<p className="font-semibold text-xl">{error}</p>
<p className="text-muted-foreground text-lg">{errorDescription}</p>
</div>
) : (
<>
<div
ref={topSentinelRef}
className="absolute bottom-0 left-0 h-px w-full"
/>
{virtualizer.getVirtualItems().map((virtualRow) => {
if (
shouldShowConversationStart &&
virtualRow.index === messageChunks.length
) {
return (
<div
key="conversation-start"
data-index={virtualRow.index}
ref={virtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
}}
>
<div className="w-full flex justify-start scale-y-[-1]">
<div className="text-sm text-foreground/55 px-2.5">
Conversation start
ref={scrollRef}
id="chat_container"
className="min-h-0 flex-1 overflow-y-auto"
style={{
overflowAnchor: "none",
paddingTop: "22px",
transform: "scaleY(-1)",
}}
onScroll={handleContainerScroll}
>
<div
className="relative w-full"
style={{ height: `${contentHeight}px` }}
>
<div
ref={topSentinelRef}
className="absolute bottom-0 left-0 h-px w-full"
/>
{virtualizer.getVirtualItems().map((virtualRow) => {
if (
shouldShowConversationStart &&
virtualRow.index === messageChunks.length
) {
return (
<div
key="conversation-start"
data-index={virtualRow.index}
ref={virtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
}}
>
<div className="w-full flex justify-start scale-y-[-1]">
<div className="text-sm text-foreground/55 px-2.5">
Conversation start
</div>
</div>
</div>
);
}
const chunkIndex = virtualRow.index;
const chunk = messageChunks[chunkIndex];
if (!chunk) {
return null;
}
return (
<div
key={chunk.key}
data-index={virtualRow.index}
className="flex flex-col"
ref={virtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
}}
>
<div className="flex flex-col scale-y-[-1]">
{chunk.messages.map((message, chunkMessageIndex) => {
const messageIndex =
chunk.startIndex + chunkMessageIndex;
const lastMessage = messages[messageIndex - 1];
const isGrouped =
lastMessage &&
lastMessage.SentBySelf === message.SentBySelf &&
Math.round(lastMessage.SendTime / 10000) ===
Math.round(message.SendTime / 10000);
return (
<Message
key={getMessageRenderKey(message)}
grouped={isGrouped}
message={message}
user={
message.SentBySelf
? (messageUsers.own ?? null)
: (messageUsers.peer ?? null)
}
/>
);
})}
</div>
</div>
</div>
);
}
const chunkIndex = virtualRow.index;
const chunk = messageChunks[chunkIndex];
if (!chunk) {
return null;
}
return (
<div
key={chunk.key}
data-index={virtualRow.index}
className="flex flex-col"
ref={virtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
}}
>
<div className="flex flex-col scale-y-[-1]">
{chunk.messages.map((message, chunkMessageIndex) => {
const messageIndex = chunk.startIndex + chunkMessageIndex;
const lastMessage = messages[messageIndex - 1];
const isGrouped =
lastMessage &&
lastMessage.SentBySelf === message.SentBySelf &&
Math.round(lastMessage.SendTime / 10000) ===
Math.round(message.SendTime / 10000);
return (
<Message
key={getMessageRenderKey(message)}
grouped={isGrouped}
message={message}
user={
message.SentBySelf
? (messageUsers.own ?? null)
: (messageUsers.peer ?? null)
}
/>
);
})}
</div>
</div>
);
})}
</div>
</div>
<div className="z-10 shrink-0">
<InputComponent setValue={setValue} value={value} />
</div>
);
})}
</div>
</div>
<div className="z-10 shrink-0">
<InputComponent setValue={setValue} value={value} />
</div>
</>
)}
</div>
);
}

View file

@ -1,7 +1,7 @@
import { z } from "zod";
import { mtp } from "@tensamin/shared/data";
export type RawMessages = z.infer<typeof mtp.messages_get.response>["messages"];
export type RawMessages = z.infer<typeof mtp.MessagesGet.response>["Messages"];
export type RawMessage = RawMessages[number];