(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

@ -19,13 +19,13 @@ export function Basic({
extra?: React.ReactNode;
}) {
return (
<Card className="animate-in fade-in duration-300 rounded-xl py-0 m-px">
<Card className="animate-in fade-in duration-300 rounded-xl py-0 h-12.5!">
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
<div className="relative shrink-0 overflow-visible">
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarImage src={user.Avatar} />
<AvatarFallback>
{user.display.slice(0, 2).toUpperCase()}
{user.Display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<Tooltip>
@ -49,7 +49,7 @@ export function Basic({
</Tooltip>
</div>
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
<p>{user.display}</p>
<p>{user.Display}</p>
</div>
<div className="pr-1">{extra}</div>
</CardHeader>
@ -58,5 +58,5 @@ export function Basic({
}
export function Loading() {
return <Skeleton className="h-12.5 rounded-2xl" />;
return <Skeleton className="h-12.5! rounded-2xl" />;
}

View file

@ -30,7 +30,7 @@ export default function Profile({ user }: { user: User }) {
void (async () => {
try {
const ownId = await load("user_id");
const privateKey = await load("private_key");
const privateKey = await load("mtp_keyring");
const ownData = await get(ownId);
const secret = await getSharedSecret(
privateKey,
@ -61,17 +61,17 @@ export default function Profile({ user }: { user: User }) {
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<Avatar className="size-10">
<AvatarImage src={user.avatar} />
<AvatarImage src={user.Avatar} />
<AvatarFallback className="text-lg">
{user.display.slice(0, 2).toUpperCase()}
{user.Display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<p className="text-lg font-semibold">{user.display}</p>
<p className="text-muted-foreground">{user.username}</p>
<p className="text-lg font-semibold">{user.Display}</p>
<p className="text-muted-foreground">{user.Username}</p>
</div>
</div>
<Text value={user.about || ""} />
<Text value={user.About || ""} />
<Button
className="h-auto justify-start gap-1.5 px-0 py-1 text-base font-medium text-white no-underline hover:no-underline"
variant="link"

View file

@ -72,7 +72,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
userId={id}
component={(user) =>
isMobile ? (
<p className="font-medium text-[1.07rem]">{user?.display}</p>
<p className="font-medium text-[1.07rem]">{user?.Display}</p>
) : (
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
<PopoverTrigger
@ -85,7 +85,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
}}
>
<p className="font-medium text-[1.07rem]">
{user?.display}
{user?.Display}
</p>
</Button>
}

View file

@ -24,7 +24,7 @@ const fetchedUser = z.object({
const formSchema = z.object({
username: z.string().min(1).max(255),
private_key: z.string().min(1).max(92),
mtp_keyring: z.string().min(1).max(92),
});
/**
@ -61,9 +61,7 @@ function parseTuFileContent(rawFileContent: string): {
? Number(userIdString.split("@")[0])
: Number(userIdString);
const domain = userIdString.includes("@")
? userIdString.split("@")[1]
: null;
const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null;
if (!userId || !privateKey) {
throw new Error("Invalid file");
@ -97,9 +95,9 @@ export default function Form() {
await save("session_id", Date.now());
await save("user_id", parsed.userId);
await save("private_key", parsed.privateKey);
await save("mtp_keyring", parsed.privateKey);
if (parsed.domain) {
await save("mtp_url", `https://${parsed.domain}/`);
await save("omega_url", `https://${parsed.domain}/`);
}
location.href = "/";
@ -240,9 +238,9 @@ export default function Form() {
await save("session_id", Date.now());
await save("user_id", user.user_id);
await save("private_key", inputParse.data.private_key);
await save("mtp_keyring", inputParse.data.mtp_keyring);
if (domain) {
await save("mtp_url", `https://${domain}/`);
await save("omega_url", `https://${domain}/`);
}
location.href = "/";
@ -273,10 +271,10 @@ export default function Form() {
await save("session_id", Date.now());
await save("user_id", userId);
await save("private_key", privateKey);
await save("mtp_keyring", privateKey);
if (domain) {
await save("mtp_url", `https://${domain}/`);
await save("omega_url", `https://${domain}/`);
}
location.href = "/";
@ -335,8 +333,8 @@ export default function Form() {
<Input required type="text" id="username" name="username" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="private_key">Private Key</Label>
<Input required type="password" id="private_key" name="private_key" />
<Label htmlFor="mtp_keyring">MTP Keyring</Label>
<Input required type="password" id="mtp_keyring" name="mtp_keyring" />
</div>
<Button className="mt-auto" type="submit">
Login

View file

@ -40,9 +40,7 @@ import type z from "zod";
import { mtp } from "@tensamin/shared/data";
import { useMTP } from "@tensamin/mtp";
type OnlineStatus = z.infer<
typeof mtp.get_user_data.response.shape.OnlineStatus
>;
type OnlineStatus = z.infer<typeof mtp.GetUserData.response.shape.OnlineStatus>;
const onlineStatusLabels: Record<OnlineStatus, string> = {
user_online: "Online",
@ -88,7 +86,7 @@ function StatusDialog({
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setDraftStatus(user.status ?? "");
setDraftStatus(user.Status ?? "");
setDraftOnlineStatus(user.OnlineStatus);
setErrorMessage("");
setSaveSucceeded(false);
@ -147,8 +145,7 @@ function StatusDialog({
OnlineStatus: draftOnlineStatus,
};
const validation =
mtp.change_user_data.request.safeParse(payload);
const validation = mtp.ChangeUserData.request.safeParse(payload);
if (!validation.success) {
setSaveSucceeded(false);
@ -159,7 +156,7 @@ function StatusDialog({
}
try {
await send("change_user_data", validation.data);
await send("ChangeUserData", validation.data);
setSaveSucceeded(true);
setErrorMessage("");
} catch (err) {
@ -229,7 +226,7 @@ export default function Sidebar() {
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => {
setDraftStatus(user.status ?? "");
setDraftStatus(user.Status ?? "");
setDraftOnlineStatus(user.OnlineStatus);
setStatusErrorMessage("");
setStatusSaveSucceeded(false);
@ -248,7 +245,7 @@ export default function Sidebar() {
open={dialogOpen}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setDraftStatus(user.status ?? "");
setDraftStatus(user.Status ?? "");
setDraftOnlineStatus(user.OnlineStatus);
setStatusErrorMessage("");
setStatusSaveSucceeded(false);

View file

@ -55,8 +55,8 @@ function AddConversationButton() {
}
// user existence check
const user = await send("get_user_data", {
username: result.data,
const user = await send("GetUserData", {
Username: result.data,
})
.then((data) => {
if (data.data.UserId === 0) {
@ -80,7 +80,7 @@ function AddConversationButton() {
// add the conv
const timeout = setTimeout(() => setLoading(true), 500);
send("add_conversation", {
send("AddConversation", {
ChatPartnerName: result.data,
})
.then(() => {

View file

@ -51,7 +51,7 @@ export default function Page() {
const avatarUploadRef = useRef<HTMLInputElement>(null);
const draftInitializedRef = useRef(false);
const effectiveAvatar =
draftUser.avatar === "none" ? undefined : draftUser.avatar;
draftUser.Avatar === "none" ? undefined : draftUser.Avatar;
const updateDraftUser = (
updater: (previous: Partial<User>) => Partial<User>,
@ -102,8 +102,8 @@ export default function Page() {
<Avatar className="size-14">
<AvatarImage src={effectiveAvatar} />
<AvatarFallback className="text-2xl">
{draftUser.display?.slice(0, 2).toUpperCase() ||
currentUser.display.slice(0, 2).toUpperCase()}
{draftUser.Display?.slice(0, 2).toUpperCase() ||
currentUser.Display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
@ -140,7 +140,7 @@ export default function Page() {
}))
}
placeholder="Display Name"
value={draftUser.display || ""}
value={draftUser.Display || ""}
/>
<Input
className="w-full"
@ -151,7 +151,7 @@ export default function Page() {
}))
}
placeholder="Username"
value={draftUser.username || ""}
value={draftUser.Username || ""}
/>
<MDInput
styled
@ -162,23 +162,23 @@ export default function Page() {
setValue={(value) =>
updateDraftUser((prev) => ({ ...prev, about: value }))
}
value={draftUser.about || ""}
value={draftUser.About || ""}
/>
<Button
onClick={async () => {
const { avatar, ...draftUsersWithoutAvatar } = draftUser;
const { Avatar, ...draftUsersWithoutAvatar } = draftUser;
const payload = {
...draftUsersWithoutAvatar,
...(typeof avatar === "string"
...(typeof Avatar === "string"
? {
avatar: avatar.startsWith("data:")
? (avatar.split(",", 2)[1] ?? "")
: avatar,
avatar: Avatar.startsWith("data:")
? (Avatar.split(",", 2)[1] ?? "")
: Avatar,
}
: {}),
};
const validation = mtp.change_user_data.request.safeParse(payload);
const validation = mtp.ChangeUserData.request.safeParse(payload);
if (!validation.success) {
setSaveSucceeded(false);
@ -189,7 +189,7 @@ export default function Page() {
}
try {
await send("change_user_data", validation.data);
await send("ChangeUserData", validation.data);
setSaveSucceeded(true);
setErrorMessage("");
} catch (err) {

View file

@ -1,124 +1,83 @@
import { useStorage } from "@tensamin/storage/context";
import { Button, Input, Label } from "@tensamin/ui";
import { useEffect, useState } from "react";
import QRCode from "qrcode";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
} from "@tensamin/ui";
export default function Page() {
return (
<div>
<QrCodeLogin />
</div>
);
}
const { save, load } = useStorage();
const [draftMtpUrl, setDraftMtpUrl] = useState("");
const [currentMtpUrl, setCurrentMtpUrl] = useState("");
// QR Code Login
const generateQR = async (text: string): Promise<string> => {
try {
const url = await QRCode.toDataURL(text, {
errorCorrectionLevel: "H",
margin: 1,
color: {
dark: "#000000FF",
light: "#FFFFFFFF",
},
});
return url;
} catch (err) {
console.error(err);
throw err;
}
};
function QrCodeLogin() {
const { load } = useStorage();
const [userId, setUserId] = useState<number>(0);
const [privateKey, setPrivateKey] = useState<string>("");
const [qrCodeBase64, setQrCodeBase64] = useState<string | undefined>(
undefined,
);
const [connectionString, setConnectionString] = useState<string | null>(null);
const [draftForcedOmikronUrl, setDraftForcedOmikronUrl] = useState("");
const [currentForcedOmikronUrl, setForcedForcedOmikronUrl] = useState("");
const [draftForcedOmikronPublicKey, setDraftForcedOmikronPublicKey] =
useState("");
const [currentForcedOmikronPublicKey, setForcedForcedOmikronPublicKey] =
useState("");
useEffect(() => {
load("private_key").then((value) => {
if (value) {
setPrivateKey(value);
}
load("omega_url").then((value) => {
setDraftMtpUrl(value);
setCurrentMtpUrl(value);
});
load("user_id").then((value) => {
if (value) {
setUserId(value);
}
load("forced_omikron_url").then((value) => {
setDraftForcedOmikronUrl(value || "");
setForcedForcedOmikronUrl(value || "");
});
load("forced_omikron_public_key").then((value) => {
setDraftForcedOmikronPublicKey(value || "");
setForcedForcedOmikronPublicKey(value || "");
});
load("mtp_url")
.then((value) => {
if (value) {
const url = new URL(value);
setConnectionString(`@${url.host}`);
} else {
setConnectionString("");
}
})
.catch(() => setConnectionString(""));
}, [load]);
useEffect(() => {
if (userId && privateKey && connectionString !== null) {
generateQR(
`tensamin://tu::${userId}${connectionString}::${privateKey}`,
).then(setQrCodeBase64);
}
}, [userId, privateKey, connectionString]);
const [qrCodeVisible, setQrCodeVisible] = useState(false);
return (
<div className="flex flex-col gap-1 pl-2">
<p>Login QR Code</p>
<div className="relative rounded-lg w-50 border-3 aspect-square overflow-hidden">
{qrCodeBase64 && (
<img className="w-full h-full" src={qrCodeBase64} alt="QR Code" />
)}
{!qrCodeVisible && (
<>
<div className="absolute top-0 left-0 w-full h-full backdrop-blur-sm bg-background/50" />
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
<AlertDialog>
<AlertDialogTrigger render={<Button>Show QR Code</Button>} />
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
Exposing this QR code is the same as sharing your .tu
file. Since changing your private key is quite tedious,
only do this when you're confident the key won't be
compromised.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
render={
<Button onClick={() => setQrCodeVisible(true)}>
Show QR Code
</Button>
}
/>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</>
)}
<div className="flex flex-col gap-8">
<p className="text-destructive">
It's best not to touch these! They can be exploited to gain access to
your account!
</p>
<div className="flex flex-col gap-2">
<Label>Omega Url</Label>
<div className="flex gap-1">
<Input
value={draftMtpUrl}
onChange={(e) => setDraftMtpUrl(e.target.value)}
/>
<Button
disabled={currentMtpUrl === draftMtpUrl}
onClick={() => {
save("omega_url", draftMtpUrl);
}}
>
Save
</Button>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Forced Omikron</Label>
<div className="flex gap-1">
<Input
placeholder="URL..."
value={draftForcedOmikronUrl || ""}
onChange={(e) => setDraftForcedOmikronUrl(e.target.value)}
/>
<Input
placeholder="Public Key..."
value={draftForcedOmikronPublicKey || ""}
onChange={(e) => setDraftForcedOmikronPublicKey(e.target.value)}
/>
<Button
disabled={
currentForcedOmikronUrl === draftForcedOmikronUrl &&
currentForcedOmikronPublicKey === draftForcedOmikronPublicKey
}
onClick={() => {
save("forced_omikron_url", draftForcedOmikronUrl);
save("forced_omikron_public_key", draftForcedOmikronPublicKey);
}}
>
Save
</Button>
</div>
</div>
</div>
);