feat(pwa): add base
All checks were successful
/ build-web (push) Successful in 5m35s
/ build-desktop (linux) (push) Successful in 9m41s
/ build-mobile (push) Successful in 20m12s
/ release (push) Successful in 1m51s
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
All checks were successful
/ build-web (push) Successful in 5m35s
/ build-desktop (linux) (push) Successful in 9m41s
/ build-mobile (push) Successful in 20m12s
/ release (push) Successful in 1m51s
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
This commit is contained in:
parent
6e977bca7e
commit
7b36218ffa
40 changed files with 4014 additions and 922 deletions
|
|
@ -8,7 +8,7 @@
|
|||
"lint": "eslint src",
|
||||
"dev": "vite",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"build": "pnpm run test && tsc -b && vite build",
|
||||
"build": "pnpm --filter @tensamin/pwa build && pnpm run test && tsc -b && vite build",
|
||||
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
"@tanstack/react-router": "^1.170.21",
|
||||
"@tanstack/react-virtual": "^3.14.9",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tensamin/pwa": "workspace:*",
|
||||
"@tensamin/cache": "workspace:*",
|
||||
"@tensamin/call": "workspace:*",
|
||||
"@tensamin/chat": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
|||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
data-pwa-navbar
|
||||
className={`${forMobile && "border-b"} pl-px w-full shrink-0 gap-2 h-13.5 flex items-center justify-between`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ import {
|
|||
useState,
|
||||
} from "react";
|
||||
import { z } from "zod";
|
||||
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||
import QrCodeScanner from "@tensamin/tauri/qrCodeScanner";
|
||||
import { subscribeTuFileLaunch } from "@tensamin/pwa/runtime";
|
||||
import {
|
||||
parseTuFileContent,
|
||||
persistMtpCredentials,
|
||||
} from "@tensamin/storage/credentials";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
const fetchedUser = z.object({
|
||||
|
|
@ -35,50 +38,6 @@ const formSchema = z.object({
|
|||
mtp_keyring: z.string().min(1).max(92),
|
||||
});
|
||||
|
||||
/**
|
||||
* Parses a .tu file payload into credentials.
|
||||
* @param rawFileContent UTF-8 file content from an uploaded .tu file.
|
||||
* @returns Parsed user id and private key credentials.
|
||||
*/
|
||||
function parseTuFileContent(rawFileContent: string): {
|
||||
userId: number;
|
||||
privateKey: string;
|
||||
domain: string | null;
|
||||
} {
|
||||
if (rawFileContent.trim().length === 0) {
|
||||
throw new Error("File is empty");
|
||||
} else if (!rawFileContent.includes("::")) {
|
||||
throw new Error("Invalid file");
|
||||
} else if (rawFileContent.split("::").length !== 2) {
|
||||
throw new Error("Invalid file");
|
||||
}
|
||||
|
||||
const left = rawFileContent.split("::")[0];
|
||||
const right = rawFileContent.split("::")[1];
|
||||
|
||||
if (left.length === 0) {
|
||||
throw new Error("Invalid file");
|
||||
} else if (right.length === 0) {
|
||||
throw new Error("Invalid file");
|
||||
} else if (isNaN(Number(left)) && !left.includes("@")) {
|
||||
throw new Error("Invalid file");
|
||||
}
|
||||
|
||||
const [userIdString, privateKeyValue] = rawFileContent.split("::");
|
||||
const privateKey = privateKeyValue.trim();
|
||||
const userId = isNaN(Number(userIdString))
|
||||
? Number(userIdString.split("@")[0])
|
||||
: Number(userIdString);
|
||||
|
||||
const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null;
|
||||
|
||||
if (!userId || !privateKey) {
|
||||
throw new Error("Invalid file");
|
||||
}
|
||||
|
||||
return { userId, privateKey, domain };
|
||||
}
|
||||
|
||||
export default function Form() {
|
||||
const isMobile = useIsMobile();
|
||||
const uploadRef = useRef<HTMLInputElement | null>(null);
|
||||
|
|
@ -92,27 +51,12 @@ export default function Form() {
|
|||
if (loginPendingRef.current) return false;
|
||||
loginPendingRef.current = true;
|
||||
try {
|
||||
if (domain) await save("omega_url", `https://${domain}/`);
|
||||
if (isTauri()) {
|
||||
const [omegaUrl, forcedOmikronUrl, forcedOmikronPublicKey] =
|
||||
await Promise.all([
|
||||
domain ? `https://${domain}/` : load("omega_url"),
|
||||
load("forced_omikron_url"),
|
||||
load("forced_omikron_public_key"),
|
||||
]);
|
||||
await invoke("mtp_store_credentials", {
|
||||
config: {
|
||||
userId,
|
||||
keyring: privateKey,
|
||||
omegaUrl,
|
||||
forcedOmikronUrl,
|
||||
forcedOmikronPublicKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
await save("mtp_keyring", privateKey, { secure: true });
|
||||
await save("session_id", Date.now());
|
||||
await save("user_id", userId);
|
||||
await persistMtpCredentials({
|
||||
storage: { load, save },
|
||||
userId,
|
||||
keyring: privateKey,
|
||||
domain,
|
||||
});
|
||||
await navigate({ to: "/", replace: true });
|
||||
return true;
|
||||
} finally {
|
||||
|
|
@ -143,6 +87,11 @@ export default function Form() {
|
|||
[persistLogin],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => subscribeTuFileLaunch(processDroppedFile),
|
||||
[processDroppedFile],
|
||||
);
|
||||
|
||||
// Handle .tu files
|
||||
const handleFileInputChange = useCallback(
|
||||
async (event: ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||
|
|
@ -281,32 +230,10 @@ export default function Form() {
|
|||
|
||||
return (
|
||||
<div className="relative flex md:flex-row flex-col gap-15">
|
||||
{isTauri() && isMobile ? (
|
||||
<>
|
||||
<QrCodeScanner
|
||||
onData={async (data) => {
|
||||
if (!data.startsWith("tensamin://tu::")) {
|
||||
toast("error", "Invalid QR code");
|
||||
return;
|
||||
}
|
||||
|
||||
const decoded = data.replace("tensamin://tu::", "");
|
||||
|
||||
try {
|
||||
const { userId, privateKey, domain } =
|
||||
parseTuFileContent(decoded);
|
||||
|
||||
await persistLogin(userId, privateKey, domain);
|
||||
} catch (error) {
|
||||
log(0, "login", "red", error);
|
||||
toast("error", "Failed to parse QR code data");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => uploadRef.current?.click()}>
|
||||
Select .tu file
|
||||
</Button>
|
||||
</>
|
||||
{isMobile ? (
|
||||
<Button onClick={() => uploadRef.current?.click()}>
|
||||
Select .tu file
|
||||
</Button>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => uploadRef.current?.click()}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { Provider as MTPProvider } from "@tensamin/mtp";
|
|||
import UserProvider from "@tensamin/user/context";
|
||||
import DeeplinkContext, { useDeeplinks } from "@tensamin/tauri/deeplinkHandler";
|
||||
import NotificationsProvider from "@tensamin/notifications/context";
|
||||
import PwaRuntime from "@tensamin/pwa/runtime";
|
||||
|
||||
import TAuthWrapper from "@tensamin/tauth/context";
|
||||
|
||||
|
|
@ -253,10 +254,14 @@ function RootShell() {
|
|||
parentThemeStorageKey={null}
|
||||
designStorageKey={null}
|
||||
>
|
||||
<div className="w-screen h-dvh overflow-hidden">
|
||||
<div
|
||||
data-pwa-root
|
||||
className="box-border w-screen h-dvh overflow-hidden"
|
||||
>
|
||||
<Toaster position={isMobile ? "top-center" : "bottom-right"} />
|
||||
<TooltipProvider>
|
||||
<Storage>
|
||||
<PwaRuntime />
|
||||
<HotkeysProvider>
|
||||
<ThemeStorageBridge />
|
||||
<LoginWrapper>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
|||
<Sidebar />
|
||||
<CallPopout />
|
||||
<div
|
||||
data-app-layout
|
||||
// Background of ui that is overlapping with the system ui
|
||||
className={cn(
|
||||
"w-full h-full min-h-0 flex flex-col overflow-hidden",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import react from "@vitejs/plugin-react";
|
|||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { mtp } from "mtp/vite";
|
||||
import { methaniumUi } from "@methanium/ui/vite";
|
||||
import { tensaminPwa } from "@tensamin/pwa/vite";
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
const appDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -125,6 +126,7 @@ export default defineConfig({
|
|||
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
||||
},
|
||||
plugins: [
|
||||
...tensaminPwa(),
|
||||
methaniumUi({ defaultThemeId: "tensamin" }),
|
||||
deepFilterAssetHeaders(resolve(appDir, "public")),
|
||||
mtp({ typeMaps: resolve(appDir, "../../mtp-type-maps/type-maps.yaml") }),
|
||||
|
|
|
|||
Loading…
Reference in a new issue