(feat): migrate ttp to mtp
(wip): crypto migration
This commit is contained in:
parent
4e69b8ef77
commit
930663d495
30 changed files with 559 additions and 749 deletions
|
|
@ -21,5 +21,5 @@ export function initTray() {
|
||||||
|
|
||||||
tray.on("click", (event) => {
|
tray.on("click", (event) => {
|
||||||
console.log(event);
|
console.log(event);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -19,13 +19,13 @@ export function Basic({
|
||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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">
|
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
|
||||||
<div className="relative shrink-0 overflow-visible">
|
<div className="relative shrink-0 overflow-visible">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -49,7 +49,7 @@ export function Basic({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
|
<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>
|
||||||
<div className="pr-1">{extra}</div>
|
<div className="pr-1">{extra}</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -58,5 +58,5 @@ export function Basic({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Loading() {
|
export function Loading() {
|
||||||
return <Skeleton className="h-12.5 rounded-2xl" />;
|
return <Skeleton className="h-12.5! rounded-2xl" />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export default function Profile({ user }: { user: User }) {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const ownId = await load("user_id");
|
const ownId = await load("user_id");
|
||||||
const privateKey = await load("private_key");
|
const privateKey = await load("mtp_keyring");
|
||||||
const ownData = await get(ownId);
|
const ownData = await get(ownId);
|
||||||
const secret = await getSharedSecret(
|
const secret = await getSharedSecret(
|
||||||
privateKey,
|
privateKey,
|
||||||
|
|
@ -61,17 +61,17 @@ export default function Profile({ user }: { user: User }) {
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Avatar className="size-10">
|
<Avatar className="size-10">
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback className="text-lg">
|
<AvatarFallback className="text-lg">
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-lg font-semibold">{user.display}</p>
|
<p className="text-lg font-semibold">{user.Display}</p>
|
||||||
<p className="text-muted-foreground">{user.username}</p>
|
<p className="text-muted-foreground">{user.Username}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Text value={user.about || ""} />
|
<Text value={user.About || ""} />
|
||||||
<Button
|
<Button
|
||||||
className="h-auto justify-start gap-1.5 px-0 py-1 text-base font-medium text-white no-underline hover:no-underline"
|
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"
|
variant="link"
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
userId={id}
|
userId={id}
|
||||||
component={(user) =>
|
component={(user) =>
|
||||||
isMobile ? (
|
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}>
|
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
|
|
@ -85,7 +85,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="font-medium text-[1.07rem]">
|
<p className="font-medium text-[1.07rem]">
|
||||||
{user?.display}
|
{user?.Display}
|
||||||
</p>
|
</p>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const fetchedUser = z.object({
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
username: z.string().min(1).max(255),
|
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.split("@")[0])
|
||||||
: Number(userIdString);
|
: Number(userIdString);
|
||||||
|
|
||||||
const domain = userIdString.includes("@")
|
const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null;
|
||||||
? userIdString.split("@")[1]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!userId || !privateKey) {
|
if (!userId || !privateKey) {
|
||||||
throw new Error("Invalid file");
|
throw new Error("Invalid file");
|
||||||
|
|
@ -97,9 +95,9 @@ export default function Form() {
|
||||||
|
|
||||||
await save("session_id", Date.now());
|
await save("session_id", Date.now());
|
||||||
await save("user_id", parsed.userId);
|
await save("user_id", parsed.userId);
|
||||||
await save("private_key", parsed.privateKey);
|
await save("mtp_keyring", parsed.privateKey);
|
||||||
if (parsed.domain) {
|
if (parsed.domain) {
|
||||||
await save("mtp_url", `https://${parsed.domain}/`);
|
await save("omega_url", `https://${parsed.domain}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
location.href = "/";
|
location.href = "/";
|
||||||
|
|
@ -240,9 +238,9 @@ export default function Form() {
|
||||||
|
|
||||||
await save("session_id", Date.now());
|
await save("session_id", Date.now());
|
||||||
await save("user_id", user.user_id);
|
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) {
|
if (domain) {
|
||||||
await save("mtp_url", `https://${domain}/`);
|
await save("omega_url", `https://${domain}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
location.href = "/";
|
location.href = "/";
|
||||||
|
|
@ -273,10 +271,10 @@ export default function Form() {
|
||||||
|
|
||||||
await save("session_id", Date.now());
|
await save("session_id", Date.now());
|
||||||
await save("user_id", userId);
|
await save("user_id", userId);
|
||||||
await save("private_key", privateKey);
|
await save("mtp_keyring", privateKey);
|
||||||
|
|
||||||
if (domain) {
|
if (domain) {
|
||||||
await save("mtp_url", `https://${domain}/`);
|
await save("omega_url", `https://${domain}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
location.href = "/";
|
location.href = "/";
|
||||||
|
|
@ -335,8 +333,8 @@ export default function Form() {
|
||||||
<Input required type="text" id="username" name="username" />
|
<Input required type="text" id="username" name="username" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="private_key">Private Key</Label>
|
<Label htmlFor="mtp_keyring">MTP Keyring</Label>
|
||||||
<Input required type="password" id="private_key" name="private_key" />
|
<Input required type="password" id="mtp_keyring" name="mtp_keyring" />
|
||||||
</div>
|
</div>
|
||||||
<Button className="mt-auto" type="submit">
|
<Button className="mt-auto" type="submit">
|
||||||
Login
|
Login
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,7 @@ import type z from "zod";
|
||||||
import { mtp } from "@tensamin/shared/data";
|
import { mtp } from "@tensamin/shared/data";
|
||||||
import { useMTP } from "@tensamin/mtp";
|
import { useMTP } from "@tensamin/mtp";
|
||||||
|
|
||||||
type OnlineStatus = z.infer<
|
type OnlineStatus = z.infer<typeof mtp.GetUserData.response.shape.OnlineStatus>;
|
||||||
typeof mtp.get_user_data.response.shape.OnlineStatus
|
|
||||||
>;
|
|
||||||
|
|
||||||
const onlineStatusLabels: Record<OnlineStatus, string> = {
|
const onlineStatusLabels: Record<OnlineStatus, string> = {
|
||||||
user_online: "Online",
|
user_online: "Online",
|
||||||
|
|
@ -88,7 +86,7 @@ function StatusDialog({
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(nextOpen) => {
|
onOpenChange={(nextOpen) => {
|
||||||
if (!nextOpen) {
|
if (!nextOpen) {
|
||||||
setDraftStatus(user.status ?? "");
|
setDraftStatus(user.Status ?? "");
|
||||||
setDraftOnlineStatus(user.OnlineStatus);
|
setDraftOnlineStatus(user.OnlineStatus);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
setSaveSucceeded(false);
|
setSaveSucceeded(false);
|
||||||
|
|
@ -147,8 +145,7 @@ function StatusDialog({
|
||||||
OnlineStatus: draftOnlineStatus,
|
OnlineStatus: draftOnlineStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
const validation =
|
const validation = mtp.ChangeUserData.request.safeParse(payload);
|
||||||
mtp.change_user_data.request.safeParse(payload);
|
|
||||||
|
|
||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
setSaveSucceeded(false);
|
setSaveSucceeded(false);
|
||||||
|
|
@ -159,7 +156,7 @@ function StatusDialog({
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await send("change_user_data", validation.data);
|
await send("ChangeUserData", validation.data);
|
||||||
setSaveSucceeded(true);
|
setSaveSucceeded(true);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -229,7 +226,7 @@ export default function Sidebar() {
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDraftStatus(user.status ?? "");
|
setDraftStatus(user.Status ?? "");
|
||||||
setDraftOnlineStatus(user.OnlineStatus);
|
setDraftOnlineStatus(user.OnlineStatus);
|
||||||
setStatusErrorMessage("");
|
setStatusErrorMessage("");
|
||||||
setStatusSaveSucceeded(false);
|
setStatusSaveSucceeded(false);
|
||||||
|
|
@ -248,7 +245,7 @@ export default function Sidebar() {
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onOpenChange={(nextOpen) => {
|
onOpenChange={(nextOpen) => {
|
||||||
if (!nextOpen) {
|
if (!nextOpen) {
|
||||||
setDraftStatus(user.status ?? "");
|
setDraftStatus(user.Status ?? "");
|
||||||
setDraftOnlineStatus(user.OnlineStatus);
|
setDraftOnlineStatus(user.OnlineStatus);
|
||||||
setStatusErrorMessage("");
|
setStatusErrorMessage("");
|
||||||
setStatusSaveSucceeded(false);
|
setStatusSaveSucceeded(false);
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ function AddConversationButton() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// user existence check
|
// user existence check
|
||||||
const user = await send("get_user_data", {
|
const user = await send("GetUserData", {
|
||||||
username: result.data,
|
Username: result.data,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.data.UserId === 0) {
|
if (data.data.UserId === 0) {
|
||||||
|
|
@ -80,7 +80,7 @@ function AddConversationButton() {
|
||||||
// add the conv
|
// add the conv
|
||||||
const timeout = setTimeout(() => setLoading(true), 500);
|
const timeout = setTimeout(() => setLoading(true), 500);
|
||||||
|
|
||||||
send("add_conversation", {
|
send("AddConversation", {
|
||||||
ChatPartnerName: result.data,
|
ChatPartnerName: result.data,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export default function Page() {
|
||||||
const avatarUploadRef = useRef<HTMLInputElement>(null);
|
const avatarUploadRef = useRef<HTMLInputElement>(null);
|
||||||
const draftInitializedRef = useRef(false);
|
const draftInitializedRef = useRef(false);
|
||||||
const effectiveAvatar =
|
const effectiveAvatar =
|
||||||
draftUser.avatar === "none" ? undefined : draftUser.avatar;
|
draftUser.Avatar === "none" ? undefined : draftUser.Avatar;
|
||||||
|
|
||||||
const updateDraftUser = (
|
const updateDraftUser = (
|
||||||
updater: (previous: Partial<User>) => Partial<User>,
|
updater: (previous: Partial<User>) => Partial<User>,
|
||||||
|
|
@ -102,8 +102,8 @@ export default function Page() {
|
||||||
<Avatar className="size-14">
|
<Avatar className="size-14">
|
||||||
<AvatarImage src={effectiveAvatar} />
|
<AvatarImage src={effectiveAvatar} />
|
||||||
<AvatarFallback className="text-2xl">
|
<AvatarFallback className="text-2xl">
|
||||||
{draftUser.display?.slice(0, 2).toUpperCase() ||
|
{draftUser.Display?.slice(0, 2).toUpperCase() ||
|
||||||
currentUser.display.slice(0, 2).toUpperCase()}
|
currentUser.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|
@ -140,7 +140,7 @@ export default function Page() {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
placeholder="Display Name"
|
placeholder="Display Name"
|
||||||
value={draftUser.display || ""}
|
value={draftUser.Display || ""}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|
@ -151,7 +151,7 @@ export default function Page() {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={draftUser.username || ""}
|
value={draftUser.Username || ""}
|
||||||
/>
|
/>
|
||||||
<MDInput
|
<MDInput
|
||||||
styled
|
styled
|
||||||
|
|
@ -162,23 +162,23 @@ export default function Page() {
|
||||||
setValue={(value) =>
|
setValue={(value) =>
|
||||||
updateDraftUser((prev) => ({ ...prev, about: value }))
|
updateDraftUser((prev) => ({ ...prev, about: value }))
|
||||||
}
|
}
|
||||||
value={draftUser.about || ""}
|
value={draftUser.About || ""}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const { avatar, ...draftUsersWithoutAvatar } = draftUser;
|
const { Avatar, ...draftUsersWithoutAvatar } = draftUser;
|
||||||
const payload = {
|
const payload = {
|
||||||
...draftUsersWithoutAvatar,
|
...draftUsersWithoutAvatar,
|
||||||
...(typeof avatar === "string"
|
...(typeof Avatar === "string"
|
||||||
? {
|
? {
|
||||||
avatar: avatar.startsWith("data:")
|
avatar: Avatar.startsWith("data:")
|
||||||
? (avatar.split(",", 2)[1] ?? "")
|
? (Avatar.split(",", 2)[1] ?? "")
|
||||||
: avatar,
|
: Avatar,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const validation = mtp.change_user_data.request.safeParse(payload);
|
const validation = mtp.ChangeUserData.request.safeParse(payload);
|
||||||
|
|
||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
setSaveSucceeded(false);
|
setSaveSucceeded(false);
|
||||||
|
|
@ -189,7 +189,7 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await send("change_user_data", validation.data);
|
await send("ChangeUserData", validation.data);
|
||||||
setSaveSucceeded(true);
|
setSaveSucceeded(true);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -1,124 +1,83 @@
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
|
import { Button, Input, Label } from "@tensamin/ui";
|
||||||
import { useEffect, useState } from "react";
|
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() {
|
export default function Page() {
|
||||||
return (
|
const { save, load } = useStorage();
|
||||||
<div>
|
const [draftMtpUrl, setDraftMtpUrl] = useState("");
|
||||||
<QrCodeLogin />
|
const [currentMtpUrl, setCurrentMtpUrl] = useState("");
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// QR Code Login
|
const [draftForcedOmikronUrl, setDraftForcedOmikronUrl] = useState("");
|
||||||
const generateQR = async (text: string): Promise<string> => {
|
const [currentForcedOmikronUrl, setForcedForcedOmikronUrl] = useState("");
|
||||||
try {
|
const [draftForcedOmikronPublicKey, setDraftForcedOmikronPublicKey] =
|
||||||
const url = await QRCode.toDataURL(text, {
|
useState("");
|
||||||
errorCorrectionLevel: "H",
|
const [currentForcedOmikronPublicKey, setForcedForcedOmikronPublicKey] =
|
||||||
margin: 1,
|
useState("");
|
||||||
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);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load("private_key").then((value) => {
|
load("omega_url").then((value) => {
|
||||||
if (value) {
|
setDraftMtpUrl(value);
|
||||||
setPrivateKey(value);
|
setCurrentMtpUrl(value);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
load("user_id").then((value) => {
|
load("forced_omikron_url").then((value) => {
|
||||||
if (value) {
|
setDraftForcedOmikronUrl(value || "");
|
||||||
setUserId(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]);
|
}, [load]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userId && privateKey && connectionString !== null) {
|
|
||||||
generateQR(
|
|
||||||
`tensamin://tu::${userId}${connectionString}::${privateKey}`,
|
|
||||||
).then(setQrCodeBase64);
|
|
||||||
}
|
|
||||||
}, [userId, privateKey, connectionString]);
|
|
||||||
|
|
||||||
const [qrCodeVisible, setQrCodeVisible] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 pl-2">
|
<div className="flex flex-col gap-8">
|
||||||
<p>Login QR Code</p>
|
<p className="text-destructive">
|
||||||
<div className="relative rounded-lg w-50 border-3 aspect-square overflow-hidden">
|
It's best not to touch these! They can be exploited to gain access to
|
||||||
{qrCodeBase64 && (
|
your account!
|
||||||
<img className="w-full h-full" src={qrCodeBase64} alt="QR Code" />
|
</p>
|
||||||
)}
|
<div className="flex flex-col gap-2">
|
||||||
{!qrCodeVisible && (
|
<Label>Omega Url</Label>
|
||||||
<>
|
<div className="flex gap-1">
|
||||||
<div className="absolute top-0 left-0 w-full h-full backdrop-blur-sm bg-background/50" />
|
<Input
|
||||||
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
value={draftMtpUrl}
|
||||||
<AlertDialog>
|
onChange={(e) => setDraftMtpUrl(e.target.value)}
|
||||||
<AlertDialogTrigger render={<Button>Show QR Code</Button>} />
|
/>
|
||||||
<AlertDialogContent>
|
<Button
|
||||||
<AlertDialogHeader>
|
disabled={currentMtpUrl === draftMtpUrl}
|
||||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
onClick={() => {
|
||||||
<AlertDialogDescription>
|
save("omega_url", draftMtpUrl);
|
||||||
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
|
Save
|
||||||
compromised.
|
</Button>
|
||||||
</AlertDialogDescription>
|
</div>
|
||||||
</AlertDialogHeader>
|
</div>
|
||||||
<AlertDialogFooter>
|
<div className="flex flex-col gap-2">
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<Label>Forced Omikron</Label>
|
||||||
<AlertDialogAction
|
<div className="flex gap-1">
|
||||||
render={
|
<Input
|
||||||
<Button onClick={() => setQrCodeVisible(true)}>
|
placeholder="URL..."
|
||||||
Show QR Code
|
value={draftForcedOmikronUrl || ""}
|
||||||
</Button>
|
onChange={(e) => setDraftForcedOmikronUrl(e.target.value)}
|
||||||
}
|
/>
|
||||||
/>
|
<Input
|
||||||
</AlertDialogFooter>
|
placeholder="Public Key..."
|
||||||
</AlertDialogContent>
|
value={draftForcedOmikronPublicKey || ""}
|
||||||
</AlertDialog>
|
onChange={(e) => setDraftForcedOmikronPublicKey(e.target.value)}
|
||||||
</div>
|
/>
|
||||||
</>
|
<Button
|
||||||
)}
|
disabled={
|
||||||
|
currentForcedOmikronUrl === draftForcedOmikronUrl &&
|
||||||
|
currentForcedOmikronPublicKey === draftForcedOmikronPublicKey
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
save("forced_omikron_url", draftForcedOmikronUrl);
|
||||||
|
save("forced_omikron_public_key", draftForcedOmikronPublicKey);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ export default function InviteButton({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user.display}
|
{user.Display}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,12 @@ export default function InvitePopup({
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogContent className="flex flex-col gap-5 items-center justify-center w-65 h-80">
|
<DialogContent className="flex flex-col gap-5 items-center justify-center w-65 h-80">
|
||||||
<Avatar className="size-30">
|
<Avatar className="size-30">
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback className="text-5xl">
|
<AvatarFallback className="text-5xl">
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<p className="text-xl font-medium">{user.display}</p>
|
<p className="text-xl font-medium">{user.Display}</p>
|
||||||
<div className="w-full flex justify-center gap-3">
|
<div className="w-full flex justify-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
className="w-14 h-14"
|
className="w-14 h-14"
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ function Overlay({
|
||||||
</TransparentButton>
|
</TransparentButton>
|
||||||
)}
|
)}
|
||||||
<TransparentButton>
|
<TransparentButton>
|
||||||
<p className="text-sm">{user.display}</p>
|
<p className="text-sm">{user.Display}</p>
|
||||||
</TransparentButton>
|
</TransparentButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -209,14 +209,14 @@ export default function Base({
|
||||||
}, [participant, get]);
|
}, [participant, get]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type !== "user" || !user?.avatar) {
|
if (type !== "user" || !user?.Avatar) {
|
||||||
setAvatarBackgroundColor(undefined);
|
setAvatarBackgroundColor(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let active = true;
|
let active = true;
|
||||||
|
|
||||||
void getAverageImageColor(user.avatar).then((color) => {
|
void getAverageImageColor(user.Avatar).then((color) => {
|
||||||
if (active) {
|
if (active) {
|
||||||
setAvatarBackgroundColor(color);
|
setAvatarBackgroundColor(color);
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +225,7 @@ export default function Base({
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [type, user?.avatar]);
|
}, [type, user?.Avatar]);
|
||||||
|
|
||||||
// Avatar calc
|
// Avatar calc
|
||||||
const currentCard = useRef<HTMLDivElement>(null);
|
const currentCard = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -342,13 +342,13 @@ export default function Base({
|
||||||
height: "32cqh",
|
height: "32cqh",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback
|
<AvatarFallback
|
||||||
style={{
|
style={{
|
||||||
fontSize: "11cqh",
|
fontSize: "11cqh",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,9 @@ export default function TopBar() {
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={
|
||||||
<Avatar className="size-7">
|
<Avatar className="size-7">
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback className="text-xs">
|
<AvatarFallback className="text-xs">
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ export default function TopBar() {
|
||||||
side="bottom"
|
side="bottom"
|
||||||
portalProps={{ container: portalContainer }}
|
portalProps={{ container: portalContainer }}
|
||||||
>
|
>
|
||||||
{user.display}
|
{user.Display}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ type IncomingCallInvite = {
|
||||||
senderId: number;
|
senderId: number;
|
||||||
};
|
};
|
||||||
type CurrentCallData =
|
type CurrentCallData =
|
||||||
(z.infer<typeof mtp.call_data.response> & { exists: boolean }) | null;
|
(z.infer<typeof mtp.CallData.response> & { exists: boolean }) | null;
|
||||||
|
|
||||||
type NavigateFn = (options: {
|
type NavigateFn = (options: {
|
||||||
to: string;
|
to: string;
|
||||||
|
|
@ -626,7 +626,7 @@ export async function sendCallInvite(userId: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownUserId = (await runtime.load("user_id")) as number;
|
const ownUserId = (await runtime.load("user_id")) as number;
|
||||||
const privateKey = await runtime.load("private_key");
|
const privateKey = await runtime.load("mtp_keyring");
|
||||||
const ownPublicKey = await runtime
|
const ownPublicKey = await runtime
|
||||||
.getUser(ownUserId)
|
.getUser(ownUserId)
|
||||||
.then((data) => data.PublicKey);
|
.then((data) => data.PublicKey);
|
||||||
|
|
@ -897,7 +897,7 @@ export async function joinCall(
|
||||||
if (callSecret) {
|
if (callSecret) {
|
||||||
try {
|
try {
|
||||||
const sharedSecret = await runtime.getSharedSecret(
|
const sharedSecret = await runtime.getSharedSecret(
|
||||||
await runtime.load("private_key"),
|
await runtime.load("mtp_keyring"),
|
||||||
await runtime
|
await runtime
|
||||||
.getUser((await runtime.load("user_id")) as number)
|
.getUser((await runtime.load("user_id")) as number)
|
||||||
.then((res) => res.PublicKey),
|
.then((res) => res.PublicKey),
|
||||||
|
|
@ -1178,7 +1178,7 @@ export function useInitializeCall() {
|
||||||
// listen to call invites
|
// listen to call invites
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
subscribePush(async (message) => {
|
subscribePush(async (message) => {
|
||||||
if (message.type !== "call_invite") return;
|
if (message.type !== "CallInvite") return;
|
||||||
|
|
||||||
const { CallId, CallSecret, SenderId } = message.data as {
|
const { CallId, CallSecret, SenderId } = message.data as {
|
||||||
CallId: string;
|
CallId: string;
|
||||||
|
|
@ -1500,10 +1500,10 @@ export function useInitializeCall() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
send("call_data", { CallId: callId })
|
send("CallData", { CallId: callId })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setCurrentCallData({
|
setCurrentCallData({
|
||||||
...(data.data as z.infer<typeof mtp.call_data.response>),
|
...(data.data as z.infer<typeof mtp.CallData.response>),
|
||||||
exists: true,
|
exists: true,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export default function Preview() {
|
||||||
{data.map((user) => {
|
{data.map((user) => {
|
||||||
return (
|
return (
|
||||||
<p key={user.UserId} className="text-2xl">
|
<p key={user.UserId} className="text-2xl">
|
||||||
User: {user.display}
|
User: {user.Display}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ import type { RawMessages } from "../values";
|
||||||
*/
|
*/
|
||||||
export async function getMessages(
|
export async function getMessages(
|
||||||
send: BoundSendFn,
|
send: BoundSendFn,
|
||||||
amount: number,
|
Amount: number,
|
||||||
offset: number,
|
Offset: number,
|
||||||
UserId: number,
|
UserId: number,
|
||||||
): Promise<RawMessages> {
|
): Promise<RawMessages> {
|
||||||
const messages = await send("messages_get", {
|
const messages = await send("MessagesGet", {
|
||||||
amount: amount,
|
Amount,
|
||||||
offset: offset,
|
Offset,
|
||||||
UserId,
|
UserId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -25,5 +25,5 @@ export async function getMessages(
|
||||||
throw new Error(messages.type);
|
throw new Error(messages.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages.data.messages;
|
return messages.data.Messages;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,9 @@ export default function InputComponent({
|
||||||
log(3, "chat", "purple", "Message send init, adding live message ...");
|
log(3, "chat", "purple", "Message send init, adding live message ...");
|
||||||
|
|
||||||
const reference = addLiveMessage({
|
const reference = addLiveMessage({
|
||||||
height: 0,
|
|
||||||
NotEncrypted: true,
|
NotEncrypted: true,
|
||||||
SendTime: time,
|
SendTime: time,
|
||||||
content: currentValue,
|
Content: currentValue,
|
||||||
SentBySelf: true,
|
SentBySelf: true,
|
||||||
MessageState: "awaiting",
|
MessageState: "awaiting",
|
||||||
});
|
});
|
||||||
|
|
@ -94,9 +93,8 @@ export default function InputComponent({
|
||||||
|
|
||||||
log(3, "chat", "purple", "Content encrypted, sending message...");
|
log(3, "chat", "purple", "Content encrypted, sending message...");
|
||||||
|
|
||||||
send("message_send", {
|
send("MessageSend", {
|
||||||
height: 0,
|
Content: encryptedContext,
|
||||||
content: encryptedContext,
|
|
||||||
ReceiverId: userId,
|
ReceiverId: userId,
|
||||||
SendTime: time,
|
SendTime: time,
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,14 @@ function MessageComponent({
|
||||||
const [isValidURL, setIsValidURL] = useState(false);
|
const [isValidURL, setIsValidURL] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
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);
|
setIsValidURL(true);
|
||||||
} catch {
|
} catch {
|
||||||
setIsValidURL(false);
|
setIsValidURL(false);
|
||||||
}
|
}
|
||||||
}, [message.content]);
|
}, [message.Content]);
|
||||||
|
|
||||||
// Message states
|
// Message states
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
|
|
@ -82,13 +82,13 @@ function MessageComponent({
|
||||||
if (readConfirmations) {
|
if (readConfirmations) {
|
||||||
if (!user?.UserId) return;
|
if (!user?.UserId) return;
|
||||||
|
|
||||||
await send("message_state", {
|
await send("MessageState", {
|
||||||
ChatPartnerId: user?.UserId,
|
ChatPartnerId: user?.UserId,
|
||||||
SendTime: message.SendTime,
|
SendTime: message.SendTime,
|
||||||
MessageState: "read",
|
MessageState: "read",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await send("message_state", {
|
await send("MessageState", {
|
||||||
ChatPartnerId: user?.UserId,
|
ChatPartnerId: user?.UserId,
|
||||||
SendTime: message.SendTime,
|
SendTime: message.SendTime,
|
||||||
MessageState: "received",
|
MessageState: "received",
|
||||||
|
|
@ -131,7 +131,7 @@ function MessageComponent({
|
||||||
className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${opacityClass}`}
|
className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${opacityClass}`}
|
||||||
>
|
>
|
||||||
<MessageContextMenu
|
<MessageContextMenu
|
||||||
content={message.content}
|
content={message.Content}
|
||||||
messageId={message.SendTime}
|
messageId={message.SendTime}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -154,9 +154,9 @@ function MessageComponent({
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Avatar className="mr-1 mb-auto mt-1 w-10">
|
<Avatar className="mr-1 mb-auto mt-1 w-10">
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
)}
|
)}
|
||||||
|
|
@ -190,7 +190,7 @@ function MessageComponent({
|
||||||
</Card>
|
</Card>
|
||||||
{!grouped && (
|
{!grouped && (
|
||||||
<div className="flex items-center gap-1">
|
<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">
|
<p className="text-xs text-muted-foreground">
|
||||||
{new Date(message.SendTime).toLocaleString([], {
|
{new Date(message.SendTime).toLocaleString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
|
|
@ -214,9 +214,9 @@ function MessageComponent({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isValidURL ? (
|
{isValidURL ? (
|
||||||
<Media link={message.content} />
|
<Media link={message.Content} />
|
||||||
) : (
|
) : (
|
||||||
<Text value={message.content} />
|
<Text value={message.Content} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -232,8 +232,7 @@ function MessageComponent({
|
||||||
export default React.memo(MessageComponent, (prev, next) => {
|
export default React.memo(MessageComponent, (prev, next) => {
|
||||||
return (
|
return (
|
||||||
prev.message.SendTime === next.message.SendTime &&
|
prev.message.SendTime === next.message.SendTime &&
|
||||||
prev.message.content === next.message.content &&
|
prev.message.Content === next.message.Content &&
|
||||||
prev.message.height === next.message.height &&
|
|
||||||
prev.message.SentBySelf === next.message.SentBySelf &&
|
prev.message.SentBySelf === next.message.SentBySelf &&
|
||||||
prev.message.MessageState === next.message.MessageState &&
|
prev.message.MessageState === next.message.MessageState &&
|
||||||
prev.message.failed === next.message.failed &&
|
prev.message.failed === next.message.failed &&
|
||||||
|
|
|
||||||
|
|
@ -50,18 +50,16 @@ function updateMessageStateBySendTime<
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export default function Provider({ children }: { children: ReactNode }) {
|
||||||
* Executes Provider.
|
|
||||||
* @param props Parameter props.
|
|
||||||
* @returns unknown.
|
|
||||||
*/
|
|
||||||
export default function Provider(props: { children: ReactNode }) {
|
|
||||||
const { getSharedSecret, decryptText } = useCrypto();
|
const { getSharedSecret, decryptText } = useCrypto();
|
||||||
const { get } = useUser();
|
const { get } = useUser();
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
const { send, subscribePush } = useMTP();
|
const { send, subscribePush } = useMTP();
|
||||||
const { moveUserIdToTop } = useSession();
|
const { moveUserIdToTop } = useSession();
|
||||||
|
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [errorDescription, setErrorDescription] = useState("");
|
||||||
|
|
||||||
const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
|
const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
|
||||||
const [currentSharedSecretState, setCurrentSharedSecretState] = useState<{
|
const [currentSharedSecretState, setCurrentSharedSecretState] = useState<{
|
||||||
userId: number;
|
userId: number;
|
||||||
|
|
@ -101,21 +99,40 @@ export default function Provider(props: { children: ReactNode }) {
|
||||||
try {
|
try {
|
||||||
const recipientData = await get(userIdValue);
|
const recipientData = await get(userIdValue);
|
||||||
const ownId = await load("user_id");
|
const ownId = await load("user_id");
|
||||||
const privateKey = await load("private_key");
|
const privateKey = await load("mtp_keyring");
|
||||||
const ownData = await get(ownId);
|
const ownData = await get(ownId);
|
||||||
|
|
||||||
|
log(3, "chat", "purple", "Getting shared secret...", {
|
||||||
|
recipientData,
|
||||||
|
ownData,
|
||||||
|
});
|
||||||
|
|
||||||
const sharedSecret = await getSharedSecret(
|
const sharedSecret = await getSharedSecret(
|
||||||
privateKey,
|
privateKey,
|
||||||
ownData.PublicKey,
|
ownData.PublicKey,
|
||||||
recipientData.PublicKey,
|
recipientData.PublicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log(2, "chat", "purple", "Got shared secret", {
|
||||||
|
sharedSecret,
|
||||||
|
});
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
setCurrentSharedSecretState({
|
setCurrentSharedSecretState({
|
||||||
userId: userIdValue,
|
userId: userIdValue,
|
||||||
value: sharedSecret,
|
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) {
|
if (active) {
|
||||||
setCurrentSharedSecretState({
|
setCurrentSharedSecretState({
|
||||||
userId: userIdValue,
|
userId: userIdValue,
|
||||||
|
|
@ -132,9 +149,9 @@ export default function Provider(props: { children: ReactNode }) {
|
||||||
|
|
||||||
const getMessages = useCallback(
|
const getMessages = useCallback(
|
||||||
async (amount: number, offset: number) => {
|
async (amount: number, offset: number) => {
|
||||||
const messages = await send("messages_get", {
|
const messages = await send("MessagesGet", {
|
||||||
amount,
|
Amount: amount,
|
||||||
offset,
|
Offset: offset,
|
||||||
UserId: userIdValue,
|
UserId: userIdValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -142,7 +159,7 @@ export default function Provider(props: { children: ReactNode }) {
|
||||||
throw new Error(messages.type);
|
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);
|
const sorted = [...rawMessages].sort((a, b) => a.SendTime - b.SendTime);
|
||||||
|
|
||||||
if (sorted.length > 0) {
|
if (sorted.length > 0) {
|
||||||
|
|
@ -162,7 +179,7 @@ export default function Provider(props: { children: ReactNode }) {
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
...message,
|
...message,
|
||||||
content: await decryptText(currentSharedSecret, message.content),
|
content: await decryptText(currentSharedSecret, message.Content),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return message;
|
return message;
|
||||||
|
|
@ -214,7 +231,7 @@ export default function Provider(props: { children: ReactNode }) {
|
||||||
// Get live updates for message states
|
// Get live updates for message states
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return subscribePush((message) => {
|
return subscribePush((message) => {
|
||||||
if (message.type !== "message_state") {
|
if (message.type !== "MessageState") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,9 +335,11 @@ export default function Provider(props: { children: ReactNode }) {
|
||||||
sharedSecret: currentSharedSecret,
|
sharedSecret: currentSharedSecret,
|
||||||
userId: userIdValue,
|
userId: userIdValue,
|
||||||
inputBoxRef,
|
inputBoxRef,
|
||||||
|
error,
|
||||||
|
errorDescription,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{children}
|
||||||
</context.Provider>
|
</context.Provider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
@ -336,13 +355,10 @@ type contextType = {
|
||||||
sharedSecret: string;
|
sharedSecret: string;
|
||||||
userId: number;
|
userId: number;
|
||||||
inputBoxRef: React.RefObject<HTMLDivElement | null>;
|
inputBoxRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
error: string;
|
||||||
|
errorDescription: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes useChat.
|
|
||||||
* @param none This function has no parameters.
|
|
||||||
* @returns contextType.
|
|
||||||
*/
|
|
||||||
export function useChat(): contextType {
|
export function useChat(): contextType {
|
||||||
const ctx = useContext(context);
|
const ctx = useContext(context);
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,6 @@ type MessageChunk = {
|
||||||
startIndex: number;
|
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({
|
function shouldFetchPreviousPage({
|
||||||
entry,
|
entry,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
|
@ -80,8 +74,15 @@ function buildMessageChunks(
|
||||||
* @returns Chat screen JSX.
|
* @returns Chat screen JSX.
|
||||||
*/
|
*/
|
||||||
export default function Screen() {
|
export default function Screen() {
|
||||||
const { getMessages, liveMessages, clearLiveMessages, userId, sharedSecret } =
|
const {
|
||||||
useChat();
|
getMessages,
|
||||||
|
liveMessages,
|
||||||
|
clearLiveMessages,
|
||||||
|
userId,
|
||||||
|
sharedSecret,
|
||||||
|
error,
|
||||||
|
errorDescription,
|
||||||
|
} = useChat();
|
||||||
const { get: getUser } = useUser();
|
const { get: getUser } = useUser();
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
|
|
||||||
|
|
@ -251,16 +252,7 @@ export default function Screen() {
|
||||||
return FALLBACK_MESSAGE_HEIGHT;
|
return FALLBACK_MESSAGE_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunk = messageChunks[index];
|
return FALLBACK_MESSAGE_HEIGHT;
|
||||||
|
|
||||||
if (!chunk) {
|
|
||||||
return FALLBACK_MESSAGE_HEIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return chunk.messages.reduce(
|
|
||||||
(total, message) => total + getEstimatedMessageHeight(message),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[messageChunks, shouldShowConversationStart],
|
[messageChunks, shouldShowConversationStart],
|
||||||
);
|
);
|
||||||
|
|
@ -479,112 +471,122 @@ export default function Screen() {
|
||||||
|
|
||||||
if (!hasValidChatUser) {
|
if (!hasValidChatUser) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex items-center justify-center text-xl text-foreground/80">
|
<div className="w-full h-full flex flex-col gap-2 items-center justify-center">
|
||||||
Invalid user
|
<p className="font-semibold text-xl">Invalid User</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full min-h-0 w-full flex-col overflow-hidden">
|
<div className="relative flex h-full min-h-0 w-full flex-col overflow-hidden">
|
||||||
<div
|
{error !== "" && errorDescription !== "" ? (
|
||||||
ref={scrollRef}
|
<div className="w-full h-full flex flex-col gap-2 items-center justify-center">
|
||||||
id="chat_container"
|
<p className="font-semibold text-xl">{error}</p>
|
||||||
className="min-h-0 flex-1 overflow-y-auto"
|
<p className="text-muted-foreground text-lg">{errorDescription}</p>
|
||||||
style={{
|
</div>
|
||||||
overflowAnchor: "none",
|
) : (
|
||||||
paddingTop: "22px",
|
<>
|
||||||
transform: "scaleY(-1)",
|
|
||||||
}}
|
|
||||||
onScroll={handleContainerScroll}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="relative w-full"
|
|
||||||
style={{ height: `${contentHeight}px` }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
ref={topSentinelRef}
|
ref={scrollRef}
|
||||||
className="absolute bottom-0 left-0 h-px w-full"
|
id="chat_container"
|
||||||
/>
|
className="min-h-0 flex-1 overflow-y-auto"
|
||||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
style={{
|
||||||
if (
|
overflowAnchor: "none",
|
||||||
shouldShowConversationStart &&
|
paddingTop: "22px",
|
||||||
virtualRow.index === messageChunks.length
|
transform: "scaleY(-1)",
|
||||||
) {
|
}}
|
||||||
return (
|
onScroll={handleContainerScroll}
|
||||||
<div
|
>
|
||||||
key="conversation-start"
|
<div
|
||||||
data-index={virtualRow.index}
|
className="relative w-full"
|
||||||
ref={virtualizer.measureElement}
|
style={{ height: `${contentHeight}px` }}
|
||||||
style={{
|
>
|
||||||
position: "absolute",
|
<div
|
||||||
top: 0,
|
ref={topSentinelRef}
|
||||||
left: 0,
|
className="absolute bottom-0 left-0 h-px w-full"
|
||||||
width: "100%",
|
/>
|
||||||
transform: `translateY(${verticalOffset + virtualRow.start}px)`,
|
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
}}
|
if (
|
||||||
>
|
shouldShowConversationStart &&
|
||||||
<div className="w-full flex justify-start scale-y-[-1]">
|
virtualRow.index === messageChunks.length
|
||||||
<div className="text-sm text-foreground/55 px-2.5">
|
) {
|
||||||
Conversation start
|
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>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
const chunkIndex = virtualRow.index;
|
<div className="z-10 shrink-0">
|
||||||
const chunk = messageChunks[chunkIndex];
|
<InputComponent setValue={setValue} value={value} />
|
||||||
if (!chunk) {
|
</div>
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { mtp } from "@tensamin/shared/data";
|
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];
|
export type RawMessage = RawMessages[number];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from "react";
|
import { createContext, useContext } from "react";
|
||||||
import * as Comlink from "comlink";
|
import { crypto } from "mtp";
|
||||||
|
|
||||||
type CryptoContextType = {
|
type CryptoContextType = {
|
||||||
decrypt: (
|
decrypt: (
|
||||||
|
|
@ -19,198 +19,26 @@ type CryptoContextType = {
|
||||||
) => Promise<string>;
|
) => Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ApiRef = {
|
export const context = createContext<CryptoContextType | undefined>(undefined);
|
||||||
encrypt: (
|
|
||||||
secret: string,
|
|
||||||
input: Uint8Array<ArrayBuffer>,
|
|
||||||
) => Promise<Uint8Array<ArrayBuffer>>;
|
|
||||||
decrypt: (
|
|
||||||
secret: string,
|
|
||||||
input: Uint8Array<ArrayBuffer>,
|
|
||||||
) => Promise<Uint8Array<ArrayBuffer>>;
|
|
||||||
decryptText: (secret: string, ciphertext: string) => Promise<string>;
|
|
||||||
encryptText: (secret: string, plaintext: string) => Promise<string>;
|
|
||||||
getSharedSecret: (
|
|
||||||
ownPrivateKey: string,
|
|
||||||
ownPublicKey: string,
|
|
||||||
otherPublicKey: string,
|
|
||||||
) => Promise<string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function bytesToBase64(bytes: Uint8Array<ArrayBuffer>): string {
|
|
||||||
let binary = "";
|
|
||||||
for (const b of bytes) binary += String.fromCharCode(b);
|
|
||||||
return btoa(binary);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function base64ToBytes(base64: string): Uint8Array<ArrayBuffer> {
|
|
||||||
const binary = atob(base64);
|
|
||||||
const bytes = new Uint8Array(binary.length);
|
|
||||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const context = React.createContext<CryptoContextType | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides cryptographic actions backed by a worker without coupling to UI state.
|
|
||||||
* @param props Component props with children.
|
|
||||||
* @returns Crypto context provider JSX.
|
|
||||||
*/
|
|
||||||
export default function Provider(props: { children: React.ReactNode }) {
|
export default function Provider(props: { children: React.ReactNode }) {
|
||||||
const apiRef = React.useRef<ApiRef | null>(null);
|
return (
|
||||||
|
<context.Provider
|
||||||
const value = React.useMemo<CryptoContextType>(
|
value={{
|
||||||
() => ({
|
decrypt: crypto.decrypt,
|
||||||
encrypt: async (secret, plaintext) => {
|
encrypt: crypto.encrypt,
|
||||||
const api = apiRef.current;
|
decryptText: crypto.decryptText,
|
||||||
if (!api) throw new Error("API not initialized");
|
encryptText: crypto.encryptText,
|
||||||
return await api.encrypt(secret, plaintext);
|
getSharedSecret: crypto.getSharedSecret,
|
||||||
},
|
}}
|
||||||
decrypt: async (secret, ciphertext) => {
|
>
|
||||||
const api = apiRef.current;
|
{props.children}
|
||||||
if (!api) throw new Error("API not initialized");
|
</context.Provider>
|
||||||
return await api.decrypt(secret, ciphertext);
|
|
||||||
},
|
|
||||||
encryptText: async (secret, plaintext) => {
|
|
||||||
const api = apiRef.current;
|
|
||||||
if (!api) throw new Error("API not initialized");
|
|
||||||
return await api.encryptText(secret, plaintext);
|
|
||||||
},
|
|
||||||
decryptText: async (secret, ciphertext) => {
|
|
||||||
const api = apiRef.current;
|
|
||||||
if (!api) throw new Error("API not initialized");
|
|
||||||
return await api.decryptText(secret, ciphertext);
|
|
||||||
},
|
|
||||||
getSharedSecret: async (ownPrivateKey, ownPublicKey, otherPublicKey) => {
|
|
||||||
const api = apiRef.current;
|
|
||||||
if (!api) throw new Error("API not initialized");
|
|
||||||
return await api.getSharedSecret(
|
|
||||||
ownPrivateKey,
|
|
||||||
ownPublicKey,
|
|
||||||
otherPublicKey,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const worker = new Worker(new URL("./worker.ts", import.meta.url), {
|
|
||||||
type: "module",
|
|
||||||
});
|
|
||||||
|
|
||||||
apiRef.current = Comlink.wrap<ApiRef>(worker);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
apiRef.current = null;
|
|
||||||
worker.terminate();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <context.Provider value={value}>{props.children}</context.Provider>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates crypto action functions that safely delegate to the worker API.
|
|
||||||
* @param getApiRef Function that returns the worker API reference.
|
|
||||||
* @returns Typed crypto action functions.
|
|
||||||
*/
|
|
||||||
export function createCryptoActions(
|
|
||||||
getApiRef: () => ApiRef | null,
|
|
||||||
): CryptoContextType {
|
|
||||||
/**
|
|
||||||
* Encrypts bytes by delegating to the crypto worker API.
|
|
||||||
* @param secret Hex-encoded shared secret.
|
|
||||||
* @param input Plaintext bytes to encrypt.
|
|
||||||
* @returns Ciphertext bytes.
|
|
||||||
*/
|
|
||||||
const encrypt = async (
|
|
||||||
secret: string,
|
|
||||||
input: Uint8Array<ArrayBuffer>,
|
|
||||||
): Promise<Uint8Array<ArrayBuffer>> => {
|
|
||||||
const api = getApiRef();
|
|
||||||
if (!api) throw new Error("API not initialized");
|
|
||||||
return await api.encrypt(secret, input);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypts bytes by delegating to the crypto worker API.
|
|
||||||
* @param secret Hex-encoded shared secret.
|
|
||||||
* @param input Ciphertext bytes to decrypt.
|
|
||||||
* @returns Plaintext bytes.
|
|
||||||
*/
|
|
||||||
const decrypt = async (
|
|
||||||
secret: string,
|
|
||||||
input: Uint8Array<ArrayBuffer>,
|
|
||||||
): Promise<Uint8Array<ArrayBuffer>> => {
|
|
||||||
const api = getApiRef();
|
|
||||||
if (!api) throw new Error("API not initialized");
|
|
||||||
return await api.decrypt(secret, input);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypts plaintext text by delegating to the crypto worker API.
|
|
||||||
* @param secret Hex-encoded shared secret.
|
|
||||||
* @param plaintext Plaintext to encrypt.
|
|
||||||
* @returns Base64 ciphertext.
|
|
||||||
*/
|
|
||||||
const encryptText = async (
|
|
||||||
secret: string,
|
|
||||||
plaintext: string,
|
|
||||||
): Promise<string> => {
|
|
||||||
const api = getApiRef();
|
|
||||||
if (!api) throw new Error("API not initialized");
|
|
||||||
return await api.encryptText(secret, plaintext);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypts base64 ciphertext text by delegating to the crypto worker API.
|
|
||||||
* @param secret Hex-encoded shared secret.
|
|
||||||
* @param ciphertext Base64 ciphertext to decrypt.
|
|
||||||
* @returns Decrypted plaintext.
|
|
||||||
*/
|
|
||||||
const decryptText = async (
|
|
||||||
secret: string,
|
|
||||||
ciphertext: string,
|
|
||||||
): Promise<string> => {
|
|
||||||
const api = getApiRef();
|
|
||||||
if (!api) throw new Error("API not initialized");
|
|
||||||
return await api.decryptText(secret, ciphertext);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derives a shared secret from local and peer key material via the worker API.
|
|
||||||
* @param ownPrivateKey Local private key.
|
|
||||||
* @param ownPublicKey Local public key.
|
|
||||||
* @param otherPublicKey Peer public key.
|
|
||||||
* @returns Hex-encoded shared secret.
|
|
||||||
*/
|
|
||||||
const getSharedSecret = async (
|
|
||||||
ownPrivateKey: string,
|
|
||||||
ownPublicKey: string,
|
|
||||||
otherPublicKey: string,
|
|
||||||
): Promise<string> => {
|
|
||||||
const api = getApiRef();
|
|
||||||
if (!api) throw new Error("API not initialized");
|
|
||||||
return await api.getSharedSecret(
|
|
||||||
ownPrivateKey,
|
|
||||||
ownPublicKey,
|
|
||||||
otherPublicKey,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { encrypt, decrypt, encryptText, decryptText, getSharedSecret };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the crypto actions from the nearest provider.
|
|
||||||
* Throws when used outside of the crypto provider tree.
|
|
||||||
*/
|
|
||||||
export function useCrypto(): CryptoContextType {
|
export function useCrypto(): CryptoContextType {
|
||||||
const ctx = React.useContext(context);
|
const ctx = useContext(context);
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
throw new Error("useCrypto must be used within a CryptoProvider");
|
throw new Error("useCrypto must be used within a CryptoProvider");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,8 @@ import { isTauri } from "@tauri-apps/api/core";
|
||||||
import { onResume } from "tauri-plugin-app-events-api";
|
import { onResume } from "tauri-plugin-app-events-api";
|
||||||
import { MTPClient } from "mtp";
|
import { MTPClient } from "mtp";
|
||||||
import { type z } from "zod";
|
import { type z } from "zod";
|
||||||
import { ConnectionState } from "./values";
|
import { ConnectionState } from "mtp";
|
||||||
import createAsyncQueue, {
|
import createAsyncQueue from "@tensamin/shared/asyncQueue";
|
||||||
createQueuedFunc,
|
|
||||||
} from "@tensamin/shared/asyncQueue";
|
|
||||||
import { toast as sonnerToast } from "@tensamin/ui";
|
import { toast as sonnerToast } from "@tensamin/ui";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
|
@ -47,36 +45,6 @@ function base64ToUint8Array(b64: string) {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PUSH_TYPES = [
|
|
||||||
"message_live",
|
|
||||||
"message_state",
|
|
||||||
"call_invite",
|
|
||||||
"error_no_iota",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const WIRE_TYPES = {
|
|
||||||
temp_cool_type: "TempCoolType",
|
|
||||||
get_user_data: "GetUserData",
|
|
||||||
change_user_data: "ChangeUserData",
|
|
||||||
ping: "AppPing",
|
|
||||||
message_live: "MessageLive",
|
|
||||||
messages_get: "MessagesGet",
|
|
||||||
message_send: "MessageSend",
|
|
||||||
add_conversation: "AddConversation",
|
|
||||||
message_state: "MessageState",
|
|
||||||
load_txt_record: "LoadTxtRecord",
|
|
||||||
authenticate_app: "AuthenticateApp",
|
|
||||||
create_app: "CreateApp",
|
|
||||||
call_token: "CallToken",
|
|
||||||
call_data: "CallData",
|
|
||||||
call_invite: "CallInvite",
|
|
||||||
error_no_iota: "ErrorNoIota",
|
|
||||||
} as const satisfies Record<keyof Schemas & string, string>;
|
|
||||||
|
|
||||||
const APP_TYPES = Object.fromEntries(
|
|
||||||
Object.entries(WIRE_TYPES).map(([appType, wireType]) => [wireType, appType]),
|
|
||||||
) as Record<string, keyof Schemas & string>;
|
|
||||||
|
|
||||||
export type ProtocolMessage<
|
export type ProtocolMessage<
|
||||||
T extends keyof Schemas & string = keyof Schemas & string,
|
T extends keyof Schemas & string = keyof Schemas & string,
|
||||||
> = {
|
> = {
|
||||||
|
|
@ -139,10 +107,8 @@ function validateResponse<T extends keyof Schemas & string>(
|
||||||
type: T,
|
type: T,
|
||||||
message: { id?: number; type: string; data: unknown },
|
message: { id?: number; type: string; data: unknown },
|
||||||
): ProtocolMessage<T> {
|
): ProtocolMessage<T> {
|
||||||
const appType = APP_TYPES[message.type] ?? message.type;
|
if (message.type.startsWith("Error")) {
|
||||||
|
return message as ProtocolMessage<T>;
|
||||||
if (appType.startsWith("error")) {
|
|
||||||
return { ...message, type: appType } as ProtocolMessage<T>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = schemas[type]?.response;
|
const schema = schemas[type]?.response;
|
||||||
|
|
@ -159,7 +125,7 @@ function validateResponse<T extends keyof Schemas & string>(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
type: appType,
|
type: message.type,
|
||||||
data: parsed.data,
|
data: parsed.data,
|
||||||
} as ProtocolMessage<T>;
|
} as ProtocolMessage<T>;
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +158,7 @@ export function Provider(props: {
|
||||||
// MTP url
|
// MTP url
|
||||||
const [mtpUrl, setMtpUrl] = useState<string | null>(null);
|
const [mtpUrl, setMtpUrl] = useState<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load("mtp_url").then(setMtpUrl);
|
load("omega_url").then(setMtpUrl);
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
// Validation override functions
|
// Validation override functions
|
||||||
|
|
@ -205,7 +171,7 @@ export function Provider(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = await client.request(
|
const message = await client.request(
|
||||||
WIRE_TYPES[type],
|
type,
|
||||||
(data ?? {}) as Record<string, unknown>,
|
(data ?? {}) as Record<string, unknown>,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
@ -220,7 +186,7 @@ export function Provider(props: {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.subscribe(WIRE_TYPES[type], (message) => {
|
return client.subscribe(type, (message) => {
|
||||||
handler(validateResponse(type, message));
|
handler(validateResponse(type, message));
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -231,8 +197,13 @@ export function Provider(props: {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribers = PUSH_TYPES.map((type) =>
|
const unsubscribers = [
|
||||||
client.subscribe(WIRE_TYPES[type], (message) => {
|
"MessageLive",
|
||||||
|
"MessageState",
|
||||||
|
"CallInvite",
|
||||||
|
"ErrorNoIota",
|
||||||
|
].map((type) =>
|
||||||
|
client.subscribe(type, (message) => {
|
||||||
handler(validateResponse(type as keyof Schemas & string, message));
|
handler(validateResponse(type as keyof Schemas & string, message));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -242,23 +213,6 @@ export function Provider(props: {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// No Iota check
|
|
||||||
useEffect(() => {
|
|
||||||
if (!connected) return;
|
|
||||||
|
|
||||||
return subscribe("error_no_iota", () => {
|
|
||||||
setIdentified(false);
|
|
||||||
setIdentifying(false);
|
|
||||||
sonnerToast.error("We couldn't reach your Iota", {
|
|
||||||
description:
|
|
||||||
"Check your network connection and try restarting your Iota",
|
|
||||||
icon: null,
|
|
||||||
duration: Infinity,
|
|
||||||
closeButton: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [connected, subscribe]);
|
|
||||||
|
|
||||||
// Custom Pings
|
// Custom Pings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!connected || !identified) {
|
if (!connected || !identified) {
|
||||||
|
|
@ -268,7 +222,7 @@ export function Provider(props: {
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const originalNow = Date.now();
|
const originalNow = Date.now();
|
||||||
const data = await send("ping", { LastPing: originalNow });
|
const data = await send("Ping", { LastPing: originalNow });
|
||||||
setOwnPing(Date.now() - originalNow);
|
setOwnPing(Date.now() - originalNow);
|
||||||
|
|
||||||
const remotePing = data.data.PingIota;
|
const remotePing = data.data.PingIota;
|
||||||
|
|
@ -286,6 +240,7 @@ export function Provider(props: {
|
||||||
}, [connected, identified, send]);
|
}, [connected, identified, send]);
|
||||||
|
|
||||||
// Reconnect stuff
|
// Reconnect stuff
|
||||||
|
const resolveConnectionRef = useRef(() => {});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mtpUrl) return;
|
if (!mtpUrl) return;
|
||||||
|
|
||||||
|
|
@ -309,12 +264,10 @@ export function Provider(props: {
|
||||||
reconnectResetTimer = null;
|
reconnectResetTimer = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
let resolveConnection: (() => void) | null = null;
|
|
||||||
|
|
||||||
if (!props.blockConnection) {
|
if (!props.blockConnection) {
|
||||||
sonnerToast.promise(
|
sonnerToast.promise(
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve) => {
|
||||||
resolveConnection = resolve;
|
resolveConnectionRef.current = resolve;
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
id: "mtp-connection-toast",
|
id: "mtp-connection-toast",
|
||||||
|
|
@ -346,61 +299,64 @@ export function Provider(props: {
|
||||||
|
|
||||||
await MTPClient.init();
|
await MTPClient.init();
|
||||||
|
|
||||||
log(2, "mtp", "purple", "Fetching Omikron data.");
|
const forcedOmikronUrl = await load("forced_omikron_url");
|
||||||
const data = await fetch(
|
const forcedOmikronPublicKey = await load("forced_omikron_public_key");
|
||||||
`${mtpUrl}api/get/omikron/${await load("user_id")}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.status === 404) {
|
let url = null;
|
||||||
sonnerToast.error("We couldn't reach your Iota", {
|
let omikronPublicKey = null;
|
||||||
description:
|
if (forcedOmikronUrl && forcedOmikronPublicKey) {
|
||||||
"Check your network connection and try restarting your Iota",
|
url = forcedOmikronUrl;
|
||||||
icon: null,
|
omikronPublicKey = forcedOmikronPublicKey;
|
||||||
duration: Infinity,
|
} else {
|
||||||
closeButton: true,
|
log(2, "mtp", "purple", "Fetching Omikron data.");
|
||||||
});
|
const data = await fetch(
|
||||||
resolveConnection?.();
|
`${mtpUrl}api/get/omikron/${await load("user_id")}`,
|
||||||
cleanup();
|
);
|
||||||
return;
|
|
||||||
|
if (data.status === 404) {
|
||||||
|
sonnerToast.error("We couldn't reach your Iota", {
|
||||||
|
description:
|
||||||
|
"Check your network connection and try restarting your Iota",
|
||||||
|
icon: null,
|
||||||
|
duration: Infinity,
|
||||||
|
closeButton: true,
|
||||||
|
});
|
||||||
|
resolveConnectionRef.current?.();
|
||||||
|
cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const omikronData = (await data.json()) as {
|
||||||
|
id: number;
|
||||||
|
ip_address: string;
|
||||||
|
port: number;
|
||||||
|
public_key: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!omikronData.ip_address ||
|
||||||
|
!omikronData.port ||
|
||||||
|
!omikronData.public_key
|
||||||
|
)
|
||||||
|
throw new Error("Invalid Omikron data");
|
||||||
|
|
||||||
|
url = `https://${omikronData.ip_address}:${omikronData.port}`;
|
||||||
|
omikronPublicKey = omikronData.public_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
const omikronData = (await data.json()) as {
|
|
||||||
id: number;
|
|
||||||
ip_address: string;
|
|
||||||
port: number;
|
|
||||||
public_key: string;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
//codec.decode(new Uint8Array(await res.arrayBuffer())),
|
//codec.decode(new Uint8Array(await res.arrayBuffer())),
|
||||||
|
|
||||||
if (
|
if (!url || !omikronPublicKey)
|
||||||
!omikronData.ip_address ||
|
throw new Error("Missing Omikron URL or Public Key");
|
||||||
!omikronData.port ||
|
|
||||||
!omikronData.public_key
|
|
||||||
)
|
|
||||||
throw new Error("Invalid Omikron data");
|
|
||||||
|
|
||||||
const url = `https://${omikronData.ip_address}:${omikronData.port}`;
|
|
||||||
|
|
||||||
log(2, "mtp", "green", "Connecting to: " + url);
|
log(2, "mtp", "green", "Connecting to: " + url);
|
||||||
|
|
||||||
const client = await MTPClient.create({
|
const client = await MTPClient.create({
|
||||||
url,
|
url,
|
||||||
storage: {
|
|
||||||
getItem: (key) => {
|
|
||||||
console.log(key);
|
|
||||||
return key;
|
|
||||||
},
|
|
||||||
removeItem: (key) => {
|
|
||||||
console.log(key);
|
|
||||||
},
|
|
||||||
setItem: console.log,
|
|
||||||
},
|
|
||||||
credentials: {
|
credentials: {
|
||||||
clientId: await load("user_id"),
|
clientId: await load("user_id"),
|
||||||
keyring: base64ToUint8Array(await load("private_key")),
|
keyring: base64ToUint8Array(await load("mtp_keyring")),
|
||||||
},
|
},
|
||||||
hostPublicKey: omikronData.public_key,
|
hostPublicKey: omikronPublicKey,
|
||||||
descriptor: "client",
|
descriptor: "client",
|
||||||
pings: true,
|
pings: true,
|
||||||
logger: (event) => {
|
logger: (event) => {
|
||||||
|
|
@ -410,12 +366,24 @@ export function Provider(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type !== "Pong") {
|
if (event.type !== "Pong" && event.type !== "Ping") {
|
||||||
log(
|
log(
|
||||||
2,
|
2,
|
||||||
"mtp",
|
"mtp",
|
||||||
event.type === "state" ? "cyan" : "blue",
|
event.type === "state"
|
||||||
event.type === "state" ? event.data : event.type,
|
? "purple"
|
||||||
|
: event.direction === "recv"
|
||||||
|
? "cyan"
|
||||||
|
: event.direction === "send"
|
||||||
|
? "gray"
|
||||||
|
: "blue",
|
||||||
|
event.type === "state"
|
||||||
|
? event.data
|
||||||
|
: event.direction === "recv"
|
||||||
|
? "< " + event.type
|
||||||
|
: event.direction === "send"
|
||||||
|
? "> " + event.type
|
||||||
|
: event.type,
|
||||||
event,
|
event,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -436,19 +404,22 @@ export function Provider(props: {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authPayload = new Promise<ProtocolMessage<"temp_cool_type">>(
|
const authPayload = new Promise<
|
||||||
(resolve, reject) => {
|
ProtocolMessage<"IdentificationResponse">
|
||||||
const unsubscribe = client.subscribe("TempCoolType", (message) => {
|
>((resolve, reject) => {
|
||||||
|
const unsubscribe = client.subscribe(
|
||||||
|
"IdentificationResponse",
|
||||||
|
(message) => {
|
||||||
try {
|
try {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
resolve(validateResponse("temp_cool_type", message));
|
resolve(validateResponse("IdentificationResponse", message));
|
||||||
} catch (authPayloadError) {
|
} catch (authPayloadError) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
reject(authPayloadError);
|
reject(authPayloadError);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
},
|
);
|
||||||
);
|
});
|
||||||
|
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
|
|
||||||
|
|
@ -467,12 +438,12 @@ export function Provider(props: {
|
||||||
|
|
||||||
if (disposed || clientRef.current !== client) return;
|
if (disposed || clientRef.current !== client) return;
|
||||||
|
|
||||||
setFreshContacts(finalResponse.data.contacts);
|
setFreshContacts(finalResponse.data.Contacts);
|
||||||
setFreshCommunities(finalResponse.data.communities ?? []);
|
setFreshCommunities(finalResponse.data.Communities);
|
||||||
setFreshCalls(finalResponse.data.calls);
|
setFreshCalls(finalResponse.data.Calls);
|
||||||
setIdentifying(false);
|
setIdentifying(false);
|
||||||
setIdentified(true);
|
setIdentified(true);
|
||||||
resolveConnection?.();
|
resolveConnectionRef.current?.();
|
||||||
} catch (connectError) {
|
} catch (connectError) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
@ -492,7 +463,7 @@ export function Provider(props: {
|
||||||
id: "mtp-connection-toast",
|
id: "mtp-connection-toast",
|
||||||
description:
|
description:
|
||||||
connectError instanceof Error
|
connectError instanceof Error
|
||||||
? connectError.message
|
? connectError.message.split(":")[0]
|
||||||
: String(connectError ?? "Unknown error"),
|
: String(connectError ?? "Unknown error"),
|
||||||
icon: null,
|
icon: null,
|
||||||
duration: Infinity,
|
duration: Infinity,
|
||||||
|
|
@ -558,6 +529,24 @@ export function Provider(props: {
|
||||||
};
|
};
|
||||||
}, [mtpUrl, props.blockConnection, load]);
|
}, [mtpUrl, props.blockConnection, load]);
|
||||||
|
|
||||||
|
// No Iota check
|
||||||
|
useEffect(() => {
|
||||||
|
if (!connected) return;
|
||||||
|
|
||||||
|
return subscribe("ErrorNoIota", () => {
|
||||||
|
setIdentified(false);
|
||||||
|
setIdentifying(false);
|
||||||
|
sonnerToast.error("We couldn't reach your Iota", {
|
||||||
|
description:
|
||||||
|
"Check your network connection and try restarting your Iota",
|
||||||
|
icon: null,
|
||||||
|
duration: Infinity,
|
||||||
|
closeButton: true,
|
||||||
|
});
|
||||||
|
resolveConnectionRef.current?.();
|
||||||
|
});
|
||||||
|
}, [connected, subscribe]);
|
||||||
|
|
||||||
// Async queue
|
// Async queue
|
||||||
const loadingDescription = useMemo(() => {
|
const loadingDescription = useMemo(() => {
|
||||||
if (!mtpUrl) return "Loading connection details";
|
if (!mtpUrl) return "Loading connection details";
|
||||||
|
|
@ -587,10 +576,18 @@ export function Provider(props: {
|
||||||
}
|
}
|
||||||
}, [connected, identified, mtpUrl, send, subscribe, subscribePush, mtpRef]);
|
}, [connected, identified, mtpUrl, send, subscribe, subscribePush, mtpRef]);
|
||||||
|
|
||||||
|
const sendQueued: BoundSendFn = useMemo(
|
||||||
|
() => async (type, data, options) => {
|
||||||
|
const mtp = await mtpRef.get();
|
||||||
|
return mtp.send(type, data, options);
|
||||||
|
},
|
||||||
|
[mtpRef],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MTPContext.Provider
|
<MTPContext.Provider
|
||||||
value={{
|
value={{
|
||||||
send: createQueuedFunc(() => (contextReady ? send : null)),
|
send: sendQueued,
|
||||||
subscribe,
|
subscribe,
|
||||||
subscribePush,
|
subscribePush,
|
||||||
readyState,
|
readyState,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { useMTP } from "@tensamin/mtp";
|
||||||
import { createContext, useEffect, useContext } from "react";
|
import { createContext, useEffect, useContext } from "react";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { toast as sonnerToast } from "sonner";
|
import { toast as sonnerToast } from "sonner";
|
||||||
import { message as messageSchema } from "@tensamin/shared/data";
|
import { Message as MessageSchema } from "@tensamin/shared/data";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
|
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
|
||||||
import { isTauri } from "@tauri-apps/api/core";
|
import { isTauri } from "@tauri-apps/api/core";
|
||||||
import { useSession } from "@tensamin/storage/session";
|
import { useSession } from "@tensamin/storage/session";
|
||||||
|
|
@ -34,9 +34,9 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return subscribePush(async (ttpMessage) => {
|
return subscribePush(async (ttpMessage) => {
|
||||||
if (ttpMessage.type === "message_live") {
|
if (ttpMessage.type === "MessageLive") {
|
||||||
const { message, SenderId } = ttpMessage.data as {
|
const { message, SenderId } = ttpMessage.data as {
|
||||||
message: z.infer<typeof messageSchema>;
|
message: z.infer<typeof MessageSchema>;
|
||||||
SenderId: number;
|
SenderId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -44,18 +44,18 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
|
|
||||||
const decryptedContent = await decryptText(
|
const decryptedContent = await decryptText(
|
||||||
await getSharedSecret(
|
await getSharedSecret(
|
||||||
await load("private_key"),
|
await load("mtp_keyring"),
|
||||||
await get(await load("user_id")).then((data) => data.PublicKey),
|
await get(await load("user_id")).then((data) => data.PublicKey),
|
||||||
user.PublicKey,
|
user.PublicKey,
|
||||||
),
|
),
|
||||||
message.content,
|
message.Content,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update message state
|
// Update message state
|
||||||
if (userId === SenderId) {
|
if (userId === SenderId) {
|
||||||
addLiveMessage({
|
addLiveMessage({
|
||||||
...message,
|
...message,
|
||||||
content: decryptedContent,
|
Content: decryptedContent,
|
||||||
SentBySelf: false,
|
SentBySelf: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -67,7 +67,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
|
|
||||||
if (await load("settings.receive_confirmations")) {
|
if (await load("settings.receive_confirmations")) {
|
||||||
void send(
|
void send(
|
||||||
"message_state",
|
"MessageState",
|
||||||
{
|
{
|
||||||
MessageState: "received",
|
MessageState: "received",
|
||||||
},
|
},
|
||||||
|
|
@ -83,10 +83,10 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
const hasPermissions = await requestNotificationPermission();
|
const hasPermissions = await requestNotificationPermission();
|
||||||
|
|
||||||
if (hasPermissions) {
|
if (hasPermissions) {
|
||||||
const notification = new Notification(user.display, {
|
const notification = new Notification(user.Display, {
|
||||||
body: decryptedContent,
|
body: decryptedContent,
|
||||||
icon: user.avatar || user.display.slice(0, 2).toUpperCase(),
|
icon: user.Avatar || user.Display.slice(0, 2).toUpperCase(),
|
||||||
badge: user.avatar || user.display.slice(0, 2).toUpperCase(),
|
badge: user.Avatar || user.Display.slice(0, 2).toUpperCase(),
|
||||||
tag: `message-${user.UserId}`,
|
tag: `message-${user.UserId}`,
|
||||||
silent: true,
|
silent: true,
|
||||||
});
|
});
|
||||||
|
|
@ -100,16 +100,16 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
notification.close();
|
notification.close();
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
sonnerToast(user.display, {
|
sonnerToast(user.Display, {
|
||||||
classNames: {
|
classNames: {
|
||||||
content: "pl-4",
|
content: "pl-4",
|
||||||
},
|
},
|
||||||
description: decryptedContent,
|
description: decryptedContent,
|
||||||
icon: (
|
icon: (
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,22 @@ const fileFromMessage = z.object({
|
||||||
type: z.enum(["image", "image_top_right", "file"]),
|
type: z.enum(["image", "image_top_right", "file"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const message = z.object({
|
export const Message = z.object({
|
||||||
height: z.number(),
|
|
||||||
NotEncrypted: z.boolean().optional(),
|
NotEncrypted: z.boolean().optional(),
|
||||||
SentBySelf: z.boolean().optional(),
|
SentBySelf: z.boolean().optional(),
|
||||||
SendTime: z.number(),
|
SendTime: z.number(),
|
||||||
content: z.base64(),
|
Content: z.base64(),
|
||||||
files: z.array(fileFromMessage).optional(),
|
Files: z.array(fileFromMessage).optional(),
|
||||||
tint: z.string().length(7).startsWith("#").optional(),
|
Tint: z.string().length(7).startsWith("#").optional(),
|
||||||
avatar: z.boolean().optional(),
|
Avatar: z.boolean().optional(),
|
||||||
display: z.boolean().optional(),
|
Display: z.boolean().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"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const failedUser = {
|
export const failedUser = {
|
||||||
display: "Failed",
|
Display: "Failed",
|
||||||
IotaId: 0,
|
IotaId: 0,
|
||||||
OmikronConnections: [],
|
OmikronConnections: [],
|
||||||
OnlineStatus: "user_borked",
|
OnlineStatus: "user_borked",
|
||||||
|
|
@ -36,12 +35,12 @@ export const failedUser = {
|
||||||
SubEnd: 0,
|
SubEnd: 0,
|
||||||
SubLevel: 0,
|
SubLevel: 0,
|
||||||
UserId: 0,
|
UserId: 0,
|
||||||
username: "unknown",
|
Username: "unknown",
|
||||||
} as z.infer<typeof mtp.get_user_data.response>;
|
} as z.infer<typeof mtp.GetUserData.response>;
|
||||||
|
|
||||||
const authPayload = z.object({
|
const authPayload = z.object({
|
||||||
communities: z.array(z.object({})).optional(),
|
Communities: z.array(z.object({})).default([]),
|
||||||
contacts: z.array(
|
Contacts: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
LastMessageAt: z.number(),
|
LastMessageAt: z.number(),
|
||||||
UserId: z.number(),
|
UserId: z.number(),
|
||||||
|
|
@ -51,21 +50,21 @@ const authPayload = z.object({
|
||||||
SenderId: z.number(),
|
SenderId: z.number(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
messages: z.array(message),
|
Messages: z.array(Message),
|
||||||
}),
|
}),
|
||||||
),
|
).default([]),
|
||||||
calls: z.array(
|
Calls: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
CallId: z.string(),
|
CallId: z.string(),
|
||||||
CallSecret: z.base64().optional(),
|
CallSecret: z.base64().optional(),
|
||||||
CallMembers: z.array(z.number()),
|
CallMembers: z.array(z.number()),
|
||||||
}),
|
}),
|
||||||
),
|
).default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Contacts = z.infer<typeof authPayload.shape.contacts>;
|
export type Contacts = z.infer<typeof authPayload.shape.Contacts>;
|
||||||
export type Communities = z.infer<typeof authPayload.shape.communities>;
|
export type Communities = z.infer<typeof authPayload.shape.Communities>;
|
||||||
export type Calls = z.infer<typeof authPayload.shape.calls>;
|
export type Calls = z.infer<typeof authPayload.shape.Calls>;
|
||||||
|
|
||||||
type Base16Palette = Record<
|
type Base16Palette = Record<
|
||||||
| "base00"
|
| "base00"
|
||||||
|
|
@ -89,9 +88,9 @@ type Base16Palette = Record<
|
||||||
|
|
||||||
// MTP
|
// MTP
|
||||||
const user = z.object({
|
const user = z.object({
|
||||||
about: z.string().max(255).optional(),
|
About: z.string().max(255).optional(),
|
||||||
avatar: z.string().optional(),
|
Avatar: z.string().optional(),
|
||||||
display: z.string().min(1).max(15),
|
Display: z.string().min(1).max(15),
|
||||||
IotaId: z.number(),
|
IotaId: z.number(),
|
||||||
OmikronConnections: z.array(z.number()),
|
OmikronConnections: z.array(z.number()),
|
||||||
OmikronId: z.number().optional(),
|
OmikronId: z.number().optional(),
|
||||||
|
|
@ -107,29 +106,29 @@ const user = z.object({
|
||||||
"iota_borked",
|
"iota_borked",
|
||||||
]),
|
]),
|
||||||
PublicKey: z.base64(),
|
PublicKey: z.base64(),
|
||||||
status: z.string().max(15).optional(),
|
Status: z.string().max(15).optional(),
|
||||||
SubEnd: z.number(),
|
SubEnd: z.number(),
|
||||||
SubLevel: z.number(),
|
SubLevel: z.number(),
|
||||||
UserId: z.number(),
|
UserId: z.number(),
|
||||||
username: z.string().min(1).max(15),
|
Username: z.string().min(1).max(15),
|
||||||
});
|
});
|
||||||
export const mtp = {
|
export const mtp = {
|
||||||
temp_cool_type: {
|
IdentificationResponse: {
|
||||||
request: z.object({}).optional(),
|
request: z.object({}).optional(),
|
||||||
response: authPayload,
|
response: authPayload,
|
||||||
},
|
},
|
||||||
get_user_data: {
|
GetUserData: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
UserId: z.number().optional(),
|
UserId: z.number().optional(),
|
||||||
username: z.string().optional(),
|
Username: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
response: user,
|
response: user,
|
||||||
},
|
},
|
||||||
change_user_data: {
|
ChangeUserData: {
|
||||||
request: user.partial(),
|
request: user.partial(),
|
||||||
response: z.object({}),
|
response: z.object({}),
|
||||||
},
|
},
|
||||||
ping: {
|
Ping: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
LastPing: z.number(),
|
LastPing: z.number(),
|
||||||
}),
|
}),
|
||||||
|
|
@ -137,76 +136,75 @@ export const mtp = {
|
||||||
PingIota: z.number(),
|
PingIota: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
message_live: {
|
MessageLive: {
|
||||||
request: z.object({}).optional(),
|
request: z.object({}).optional(),
|
||||||
response: z.object({
|
response: z.object({
|
||||||
SenderId: z.number(),
|
SenderId: z.number(),
|
||||||
message,
|
Message,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
messages_get: {
|
MessagesGet: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
UserId: z.number(),
|
UserId: z.number(),
|
||||||
amount: z.number(),
|
Amount: z.number(),
|
||||||
offset: z.number(),
|
Offset: z.number(),
|
||||||
}),
|
}),
|
||||||
response: z.object({
|
response: z.object({
|
||||||
messages: z.array(message),
|
Messages: z.array(Message),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
message_send: {
|
MessageSend: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
height: z.number(),
|
Content: z.base64(),
|
||||||
content: z.base64(),
|
|
||||||
ReceiverId: z.number(),
|
ReceiverId: z.number(),
|
||||||
SendTime: z.number(),
|
SendTime: z.number(),
|
||||||
files: z.array(fileFromMessage).optional(),
|
Files: z.array(fileFromMessage).optional(),
|
||||||
}),
|
}),
|
||||||
response: z.object({}),
|
response: z.object({}),
|
||||||
},
|
},
|
||||||
add_conversation: {
|
AddConversation: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
ChatPartnerId: z.number().optional(),
|
ChatPartnerId: z.number().optional(),
|
||||||
ChatPartnerName: z.string().min(1).max(15).optional(),
|
ChatPartnerName: z.string().min(1).max(15).optional(),
|
||||||
}),
|
}),
|
||||||
response: z.object({}),
|
response: z.object({}),
|
||||||
},
|
},
|
||||||
message_state: {
|
MessageState: {
|
||||||
request: z
|
request: z
|
||||||
.object({
|
.object({
|
||||||
ChatPartnerId: z.number(),
|
ChatPartnerId: z.number(),
|
||||||
SendTime: z.number(),
|
SendTime: z.number(),
|
||||||
MessageState: message.shape.MessageState,
|
MessageState: Message.shape.MessageState,
|
||||||
})
|
})
|
||||||
.or(
|
.or(
|
||||||
z.object({
|
z.object({
|
||||||
MessageState: message.shape.MessageState,
|
MessageState: Message.shape.MessageState,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
response: z.object({
|
response: z.object({
|
||||||
ChatPartnerId: z.number(),
|
ChatPartnerId: z.number(),
|
||||||
MessageState: message.shape.MessageState,
|
MessageState: Message.shape.MessageState,
|
||||||
SendTime: z.number(),
|
SendTime: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
load_txt_record: {
|
LoadTxtRecord: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
path: z.string(),
|
Path: z.string(),
|
||||||
}),
|
}),
|
||||||
response: z.object({
|
response: z.object({
|
||||||
content: z.string(),
|
Content: z.string(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
authenticate_app: {
|
AuthenticateApp: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
AppIdentifier: z.string(),
|
AppIdentifier: z.string(),
|
||||||
}),
|
}),
|
||||||
response: z.object({
|
response: z.object({
|
||||||
challenge: z.base64(),
|
Challenge: z.base64(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
create_app: {
|
CreateApp: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
AppPublicKey: z.base64(),
|
AppPublicKey: z.base64(),
|
||||||
AppIdentifier: z.string(),
|
AppIdentifier: z.string(),
|
||||||
|
|
@ -215,7 +213,7 @@ export const mtp = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Calls
|
// Calls
|
||||||
call_token: {
|
CallToken: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
CallId: z.string(),
|
CallId: z.string(),
|
||||||
}),
|
}),
|
||||||
|
|
@ -223,7 +221,7 @@ export const mtp = {
|
||||||
CallToken: z.string(),
|
CallToken: z.string(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
call_data: {
|
CallData: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
CallId: z.string(),
|
CallId: z.string(),
|
||||||
}),
|
}),
|
||||||
|
|
@ -231,7 +229,7 @@ export const mtp = {
|
||||||
UserIds: z.array(z.number()),
|
UserIds: z.array(z.number()),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
call_invite: {
|
CallInvite: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
CallId: z.string(),
|
CallId: z.string(),
|
||||||
CallSecret: z.base64(),
|
CallSecret: z.base64(),
|
||||||
|
|
@ -243,7 +241,7 @@ export const mtp = {
|
||||||
SenderId: z.number().optional(),
|
SenderId: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
error_no_iota: {
|
ErrorNoIota: {
|
||||||
request: z.object({}).optional(),
|
request: z.object({}).optional(),
|
||||||
response: z.object({}),
|
response: z.object({}),
|
||||||
},
|
},
|
||||||
|
|
@ -255,7 +253,7 @@ export type MTP = typeof mtp;
|
||||||
export interface Storage extends SettingsStorageDefaults {
|
export interface Storage extends SettingsStorageDefaults {
|
||||||
session_id: number;
|
session_id: number;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
private_key: string;
|
mtp_keyring: string;
|
||||||
ppandtos_done: boolean;
|
ppandtos_done: boolean;
|
||||||
accepted_terms_of_service: boolean;
|
accepted_terms_of_service: boolean;
|
||||||
accepted_privacy_policy: boolean;
|
accepted_privacy_policy: boolean;
|
||||||
|
|
@ -265,7 +263,9 @@ export interface Storage extends SettingsStorageDefaults {
|
||||||
legal_docs: z.infer<typeof legalDocsSchema>;
|
legal_docs: z.infer<typeof legalDocsSchema>;
|
||||||
cached_contacts: Contacts;
|
cached_contacts: Contacts;
|
||||||
cached_communities: Communities;
|
cached_communities: Communities;
|
||||||
mtp_url: string;
|
omega_url: string;
|
||||||
|
forced_omikron_url: string | undefined;
|
||||||
|
forced_omikron_public_key: string | undefined;
|
||||||
call_mute_range_start: number;
|
call_mute_range_start: number;
|
||||||
call_mute_range_end: number;
|
call_mute_range_end: number;
|
||||||
theme_color: string;
|
theme_color: string;
|
||||||
|
|
@ -287,7 +287,7 @@ export interface Storage extends SettingsStorageDefaults {
|
||||||
export const storageDefaults: Storage = {
|
export const storageDefaults: Storage = {
|
||||||
session_id: 0,
|
session_id: 0,
|
||||||
user_id: 0,
|
user_id: 0,
|
||||||
private_key: "",
|
mtp_keyring: "",
|
||||||
ppandtos_done: false,
|
ppandtos_done: false,
|
||||||
accepted_terms_of_service: false,
|
accepted_terms_of_service: false,
|
||||||
accepted_privacy_policy: false,
|
accepted_privacy_policy: false,
|
||||||
|
|
@ -314,7 +314,9 @@ export const storageDefaults: Storage = {
|
||||||
},
|
},
|
||||||
cached_contacts: [],
|
cached_contacts: [],
|
||||||
cached_communities: [],
|
cached_communities: [],
|
||||||
mtp_url: "https://omega.tensamin.net",
|
omega_url: "https://omega.tensamin.net",
|
||||||
|
forced_omikron_url: undefined,
|
||||||
|
forced_omikron_public_key: undefined,
|
||||||
call_mute_range_start: -55,
|
call_mute_range_start: -55,
|
||||||
call_mute_range_end: -45,
|
call_mute_range_end: -45,
|
||||||
theme_color: "",
|
theme_color: "",
|
||||||
|
|
@ -355,7 +357,7 @@ export const storageDefaults: Storage = {
|
||||||
|
|
||||||
// User Status
|
// User Status
|
||||||
export function getStatusColor(
|
export function getStatusColor(
|
||||||
status: z.infer<typeof mtp.get_user_data.response.shape.OnlineStatus>,
|
status: z.infer<typeof mtp.GetUserData.response.shape.OnlineStatus>,
|
||||||
) {
|
) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "user_online":
|
case "user_online":
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export default function SessionProvider({ children }: { children: ReactNode }) {
|
||||||
const newUser = {
|
const newUser = {
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
LastMessageAt: new Date().getTime(),
|
LastMessageAt: new Date().getTime(),
|
||||||
messages: [],
|
Messages: [],
|
||||||
} satisfies Contacts[0];
|
} satisfies Contacts[0];
|
||||||
|
|
||||||
return [newUser, ...prevContacts];
|
return [newUser, ...prevContacts];
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
const [redirect, setRedirect] = useState<string | null>(null);
|
const [redirect, setRedirect] = useState<string | null>(null);
|
||||||
const [challenge, setChallenge] = useState<string | null>(null);
|
const [challenge, setChallenge] = useState<string | null>(null);
|
||||||
const [appPublicKey, setAppPublicKey] = useState<string | null>(null);
|
const [appPublicKey, setAppPublicKey] = useState<string | null>(null);
|
||||||
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
const [allowChildern, setAllowChildern] = useState(false);
|
const [allowChildern, setAllowChildern] = useState(false);
|
||||||
|
|
||||||
const { deeplinks } = useDeeplinks();
|
const { deeplinks } = useDeeplinks();
|
||||||
|
|
@ -44,9 +45,9 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
// Get Data
|
// Get Data
|
||||||
const user = await get(await load("user_id"));
|
const user = await get(await load("user_id"));
|
||||||
const {
|
const {
|
||||||
data: { content: appPublicKeyHash },
|
data: { Content: appPublicKeyHash },
|
||||||
} = await send("load_txt_record", {
|
} = await send("LoadTxtRecord", {
|
||||||
path: "tauth." + identifier,
|
Path: "tauth." + identifier,
|
||||||
});
|
});
|
||||||
|
|
||||||
const verifiedAppPublicKeyHash = await sha256Hex(appPublicKey);
|
const verifiedAppPublicKeyHash = await sha256Hex(appPublicKey);
|
||||||
|
|
@ -61,7 +62,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
// Get Shared Secret
|
// Get Shared Secret
|
||||||
const sharedSecret = await getSharedSecret(
|
const sharedSecret = await getSharedSecret(
|
||||||
await load("private_key"),
|
await load("mtp_keyring"),
|
||||||
user.PublicKey,
|
user.PublicKey,
|
||||||
appPublicKey,
|
appPublicKey,
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
|
|
@ -86,12 +87,13 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
finalUrl.searchParams.set("userId", String(await load("user_id")));
|
finalUrl.searchParams.set("userId", String(await load("user_id")));
|
||||||
finalUrl.searchParams.set("challenge", solvedChallenge);
|
finalUrl.searchParams.set("challenge", solvedChallenge);
|
||||||
finalUrl.searchParams.set("originalChallenge", challenge);
|
finalUrl.searchParams.set("originalChallenge", challenge);
|
||||||
|
finalUrl.searchParams.set(
|
||||||
const session = new Date().getTime();
|
"sessionId",
|
||||||
finalUrl.searchParams.set("sessionId", String(session));
|
String(sessionId || new Date().getTime()),
|
||||||
|
);
|
||||||
|
|
||||||
// Save Session
|
// Save Session
|
||||||
await send("create_app", {
|
await send("CreateApp", {
|
||||||
AppPublicKey: appPublicKey,
|
AppPublicKey: appPublicKey,
|
||||||
AppIdentifier: identifier,
|
AppIdentifier: identifier,
|
||||||
});
|
});
|
||||||
|
|
@ -105,12 +107,13 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
setRedirect(null);
|
setRedirect(null);
|
||||||
setChallenge(null);
|
setChallenge(null);
|
||||||
setAppPublicKey(null);
|
setAppPublicKey(null);
|
||||||
|
setSessionId(null);
|
||||||
|
|
||||||
toast("success", "App authorized successfully");
|
toast("success", "App authorized successfully");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
navigate({
|
navigate({
|
||||||
to: finalUrl.toString(),
|
href: finalUrl.toString(),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -124,6 +127,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
setRedirect(null);
|
setRedirect(null);
|
||||||
setChallenge(null);
|
setChallenge(null);
|
||||||
setAppPublicKey(null);
|
setAppPublicKey(null);
|
||||||
|
setSessionId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -156,6 +160,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
const redirect = params.get("redirect");
|
const redirect = params.get("redirect");
|
||||||
const challenge = params.get("challenge");
|
const challenge = params.get("challenge");
|
||||||
const appPublicKey = params.get("public_key");
|
const appPublicKey = params.get("public_key");
|
||||||
|
const urlSessionId = params.get("sessionId");
|
||||||
if (!identifier || !redirect) {
|
if (!identifier || !redirect) {
|
||||||
setAllowChildern(true);
|
setAllowChildern(true);
|
||||||
return;
|
return;
|
||||||
|
|
@ -165,6 +170,10 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
const newUrl = new URL(redirect);
|
const newUrl = new URL(redirect);
|
||||||
newUrl.searchParams.set("userId", String(userId));
|
newUrl.searchParams.set("userId", String(userId));
|
||||||
|
newUrl.searchParams.set(
|
||||||
|
"sessionId",
|
||||||
|
String(urlSessionId || new Date().getTime()),
|
||||||
|
);
|
||||||
window.location.href = newUrl.toString();
|
window.location.href = newUrl.toString();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -181,6 +190,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
setRedirect(redirect);
|
setRedirect(redirect);
|
||||||
setChallenge(hexToBase64(challenge || ""));
|
setChallenge(hexToBase64(challenge || ""));
|
||||||
setAppPublicKey(appPublicKey);
|
setAppPublicKey(appPublicKey);
|
||||||
|
setSessionId(urlSessionId);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
});
|
});
|
||||||
}, [searchStr, load]);
|
}, [searchStr, load]);
|
||||||
|
|
@ -192,12 +202,14 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
const identifier = url.searchParams.get("identifier");
|
const identifier = url.searchParams.get("identifier");
|
||||||
const redirect = url.searchParams.get("redirect");
|
const redirect = url.searchParams.get("redirect");
|
||||||
const appPublicKey = url.searchParams.get("public_key");
|
const appPublicKey = url.searchParams.get("public_key");
|
||||||
|
const urlSessionId = url.searchParams.get("sessionId");
|
||||||
|
|
||||||
if (identifier && redirect && appPublicKey) {
|
if (identifier && redirect && appPublicKey) {
|
||||||
setIdentifier(identifier);
|
setIdentifier(identifier);
|
||||||
setRedirect(redirect);
|
setRedirect(redirect);
|
||||||
setChallenge(hexToBase64(url.searchParams.get("challenge") || ""));
|
setChallenge(hexToBase64(url.searchParams.get("challenge") || ""));
|
||||||
setAppPublicKey(appPublicKey);
|
setAppPublicKey(appPublicKey);
|
||||||
|
setSessionId(urlSessionId);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +227,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
setRedirect(null);
|
setRedirect(null);
|
||||||
setChallenge(null);
|
setChallenge(null);
|
||||||
setAppPublicKey(null);
|
setAppPublicKey(null);
|
||||||
|
setSessionId(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -251,6 +264,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||||
setRedirect(null);
|
setRedirect(null);
|
||||||
setChallenge(null);
|
setChallenge(null);
|
||||||
setAppPublicKey(null);
|
setAppPublicKey(null);
|
||||||
|
setSessionId(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Deny
|
Deny
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useMTP } from "@tensamin/mtp";
|
||||||
import { mtp as schemas } from "@tensamin/shared/data";
|
import { mtp as schemas } from "@tensamin/shared/data";
|
||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
export type User = z.infer<typeof schemas.get_user_data.response>;
|
export type User = z.infer<typeof schemas.GetUserData.response>;
|
||||||
|
|
||||||
interface contextValue {
|
interface contextValue {
|
||||||
get(userId: number): Promise<User>;
|
get(userId: number): Promise<User>;
|
||||||
|
|
@ -47,11 +47,11 @@ export default function UserProvider(props: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = (async () => {
|
const request = (async () => {
|
||||||
const userData = await send("get_user_data", { UserId: userId });
|
const userData = await send("GetUserData", { UserId: userId });
|
||||||
const user = {
|
const user = {
|
||||||
...userData.data,
|
...userData.data,
|
||||||
avatar: userData.data.avatar
|
avatar: userData.data.Avatar
|
||||||
? `data:image/webp;base64,${atob(userData.data.avatar)}`
|
? `data:image/webp;base64,${atob(userData.data.Avatar)}`
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
|
|
@ -6,7 +6,7 @@ settings:
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
'@tensamin/ui': https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz
|
'@tensamin/ui': https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz
|
||||||
mtp: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz
|
mtp: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
|
|
@ -16,8 +16,8 @@ importers:
|
||||||
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz
|
specifier: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz
|
||||||
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
version: https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz(@date-fns/tz@1.5.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)(typescript@6.0.3)
|
||||||
mtp:
|
mtp:
|
||||||
specifier: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz
|
specifier: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz
|
||||||
version: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz
|
version: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
|
@ -661,8 +661,8 @@ importers:
|
||||||
specifier: ^1.14.0
|
specifier: ^1.14.0
|
||||||
version: 1.23.0(react@19.2.7)
|
version: 1.23.0(react@19.2.7)
|
||||||
mtp:
|
mtp:
|
||||||
specifier: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz
|
specifier: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz
|
||||||
version: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz
|
version: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz
|
||||||
react:
|
react:
|
||||||
specifier: ^19.2.0
|
specifier: ^19.2.0
|
||||||
version: 19.2.7
|
version: 19.2.7
|
||||||
|
|
@ -3752,8 +3752,8 @@ packages:
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz:
|
mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz:
|
||||||
resolution: {integrity: sha512-ixykS56llG6Azoxn4ff2v83LlrkCsd/s5wXmEUsxGtM63v8cYvX/HXh1AmWfJeaL0QZiGw1QyyOC0vplXZjwVg==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz}
|
resolution: {integrity: sha512-jv3tONmaEMVyoX3HyQhZfkt46MkoaZAkoy3B9HjFUOh6ff42yutAKgt/WQgX9MP5AyFd8oVlvTgMpaOGk7046A==, tarball: https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz}
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
|
|
||||||
nanoid@3.3.15:
|
nanoid@3.3.15:
|
||||||
|
|
@ -7879,7 +7879,7 @@ snapshots:
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz: {}
|
mtp@https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz: {}
|
||||||
|
|
||||||
nanoid@3.3.15: {}
|
nanoid@3.3.15: {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@ allowBuilds:
|
||||||
esbuild: true
|
esbuild: true
|
||||||
overrides:
|
overrides:
|
||||||
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz"
|
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/releases/download/0.0.40/tensamin-ui.tgz"
|
||||||
mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-2e7c0b4/mtp-0.1.0.tgz"
|
mtp: "https://git.methanium.net/methanium/mtp/releases/download/0.1.0-dev-fdc694b/mtp-0.1.0.tgz"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue