diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 24d7cc6..5bb82f5 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,8 @@ { - "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] + "recommendations": [ + "tauri-apps.tauri-vscode", + "rust-lang.rust-analyzer", + "bradlc.vscode-tailwindcss", + "antfu.vite" + ] } diff --git a/apps/tauri/package.json b/apps/tauri/package.json index bd89438..00d3091 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -5,7 +5,9 @@ "type": "module", "exports": { "./context": "./src/context.tsx", - "./controls": "./src/windowControls.tsx" + "./controls": "./src/windowControls.tsx", + "./deeplinkHandler": "./src/deeplinkHandler.tsx", + "./qrCodeScanner": "./src/qrCodeScanner.tsx" }, "scripts": { "dev:mobile:raw": "tauri android dev", @@ -22,8 +24,11 @@ }, "dependencies": { "@tauri-apps/api": "^2", + "@tauri-apps/plugin-barcode-scanner": "~2", + "@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-opener": "^2", "@tensamin/ui": "workspace:*", + "@tensamin/shared": "workspace:*", "lucide-react": "^1.7.0", "react": "^19.2.0", "react-dom": "^19.2.0" diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index ead86e0..f7e2a6c 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -77,7 +77,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -88,7 +88,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -625,6 +625,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -733,6 +753,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -954,6 +980,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "document-features" version = "0.2.12" @@ -1653,6 +1688,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -2700,6 +2741,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3389,6 +3440,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -4235,6 +4296,41 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-barcode-scanner" +version = "2.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485cbcf227f04117e930be748ea71d835900466dcd1d455d5ec284d36107a305" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "2.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db49816aee496a9b200d55b55ab6ae73fd50847c79f2fabc7ee20871fa75c95" +dependencies = [ + "dunce", + "plist", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils 2.8.3 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 2.0.18", + "tracing", + "url", + "windows-registry", + "windows-result 0.3.4", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.3" @@ -4459,6 +4555,8 @@ dependencies = [ "serde_json", "tauri", "tauri-build 2.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tauri-plugin-barcode-scanner", + "tauri-plugin-deep-link", "tauri-plugin-opener", ] @@ -4533,6 +4631,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -5426,6 +5533,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-result" version = "0.3.4" diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index c501b2c..02ee39a 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri-build = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +tauri-plugin-deep-link = "2" [target.'cfg(target_os = "android")'.dependencies.tauri] version = "2" @@ -42,6 +43,9 @@ version = "2" features = ["x11", "common-controls-v6", "cef", "compression", "dynamic-acl"] default-features = false +[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] +tauri-plugin-barcode-scanner = "2" + [patch.crates-io.tauri] git = "https://github.com/tauri-apps/tauri" branch = "feat/cef" diff --git a/apps/tauri/src-tauri/capabilities/default.json b/apps/tauri/src-tauri/capabilities/default.json index 63f70d5..1f5ace4 100644 --- a/apps/tauri/src-tauri/capabilities/default.json +++ b/apps/tauri/src-tauri/capabilities/default.json @@ -12,6 +12,8 @@ "core:window:allow-start-dragging", "core:window:allow-close", "core:window:allow-toggle-maximize", - "core:window:allow-minimize" + "core:window:allow-minimize", + "core:event:default", + "deep-link:default" ] } \ No newline at end of file diff --git a/apps/tauri/src-tauri/capabilities/mobile.json b/apps/tauri/src-tauri/capabilities/mobile.json new file mode 100644 index 0000000..4f08f6c --- /dev/null +++ b/apps/tauri/src-tauri/capabilities/mobile.json @@ -0,0 +1,15 @@ +{ + "identifier": "mobile-capability", + "platforms": [ + "android", + "iOS" + ], + "windows": [ + "main" + ], + "permissions": [ + "barcode-scanner:default", + "barcode-scanner:allow-scan", + "barcode-scanner:allow-cancel" + ] +} \ No newline at end of file diff --git a/apps/tauri/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/apps/tauri/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 3e1bfe8..697b23b 100644 --- a/apps/tauri/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/apps/tauri/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,21 @@ + + + + + + + + + + + + + + + String { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() - .plugin(tauri_plugin_opener::init()) + let builder = tauri::Builder::default() + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_opener::init()); + + #[cfg(any(target_os = "ios", target_os = "android"))] + let builder = builder.plugin(tauri_plugin_barcode_scanner::init()); + + builder + .setup(|_app| { + #[cfg(any(target_os = "linux", windows))] + { + use tauri_plugin_deep_link::DeepLinkExt; + _app.deep_link().register_all()?; + } + Ok(()) + }) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/apps/tauri/src-tauri/tauri.conf.json b/apps/tauri/src-tauri/tauri.conf.json index 4c81524..d3861d9 100644 --- a/apps/tauri/src-tauri/tauri.conf.json +++ b/apps/tauri/src-tauri/tauri.conf.json @@ -34,5 +34,18 @@ "icons/icon.icns", "icons/icon.ico" ] + }, + "plugins": { + "deep-link": { + "desktop": { + "schemes": ["tensamin"] + }, + "mobile": [ + { + "scheme": ["tensamin"], + "appLink": false + } + ] + } } } diff --git a/apps/tauri/src/deeplinkHandler.tsx b/apps/tauri/src/deeplinkHandler.tsx new file mode 100644 index 0000000..e9d0d47 --- /dev/null +++ b/apps/tauri/src/deeplinkHandler.tsx @@ -0,0 +1,71 @@ +import { + createContext, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; +import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"; + +type DeeplinkContextValue = { + deeplinks: readonly string[]; +}; + +export const deeplinkContext = createContext( + undefined, +); + +export function useDeeplinks() { + const value = useContext(deeplinkContext); + if (!value) { + throw new Error("useDeeplinks must be used within DeeplinkProvider"); + } + + return value; +} + +export default function DeeplinkProvider({ + children, +}: { + children: ReactNode; +}) { + const [deeplinks, setDeeplinks] = useState([]); + + useEffect(() => { + let mounted = true; + let unlisten: (() => void) | undefined; + + getCurrent().then((links) => { + if (mounted && links) { + setDeeplinks((prev) => [...prev, ...links]); + } + }); + + onOpenUrl((links) => { + if (mounted) { + setDeeplinks((prev) => [...prev, ...links]); + } + }).then((fn) => { + unlisten = fn; + }); + + return () => { + mounted = false; + unlisten?.(); + }; + }, []); + + useEffect(() => { + console.log(deeplinks); + }, [deeplinks]); + + return ( + + {children} + + ); +} diff --git a/apps/tauri/src/qrCodeScanner.tsx b/apps/tauri/src/qrCodeScanner.tsx new file mode 100644 index 0000000..fe76f45 --- /dev/null +++ b/apps/tauri/src/qrCodeScanner.tsx @@ -0,0 +1,38 @@ +import { + scan, + Format, + requestPermissions, +} from "@tauri-apps/plugin-barcode-scanner"; +import { Button } from "@tensamin/ui/cmp/button"; + +import { toast } from "@tensamin/shared/log"; + +export default function QrCodeScanner({ + onData, +}: { + onData: (data: string) => void; +}) { + return ( + + ); +} diff --git a/apps/web/package.json b/apps/web/package.json index f06d421..160de52 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,21 +17,22 @@ "@tailwindcss/vite": "^4.2.1", "@tanstack/react-router": "^1.0.0", "@tanstack/react-virtual": "^3.0.0", + "@tauri-apps/api": "^2", "@tensamin/call": "workspace:*", "@tensamin/chat": "workspace:*", "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", + "@tensamin/tauri": "workspace:*", "@tensamin/ttp": "workspace:*", "@tensamin/ui": "workspace:*", "@tensamin/user": "workspace:*", - "@tensamin/tauri": "workspace:*", - "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "comlink": "^4.4.2", "framer-motion": "^12.38.0", "lucide-react": "^0.564.0", + "qrcode": "^1.5.4", "react": "^19.2.0", "react-dom": "^19.2.0", "shadcn": "^4.0.7", @@ -44,10 +45,11 @@ "zod": "^4.3.6" }, "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.0.4", - "@eslint/js": "^10.0.1", "eslint": "^10.0.3", "globals": "^17.4.0", "typescript": "~5.9.3", diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index da54a40..329d5e6 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -1,5 +1,5 @@ import { Button } from "@tensamin/ui/cmp/button"; -import { ArrowLeft, House, Phone, User } from "lucide-react"; +import { ArrowLeft, House, Phone, Settings, User } from "lucide-react"; import { useLocation, useNavigate, useSearch } from "@tanstack/react-router"; import { useCall } from "@tensamin/call/context"; import Wrapper from "@tensamin/user/wrapper"; @@ -22,6 +22,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { const { contacts } = useTTP(); const { pathname } = useLocation(); + const { id } = useSearch({ strict: false }); const currentCalls = id ? contacts?.[id]?.calls || [] : []; @@ -43,13 +44,23 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { ) : ( - + <> + + + )} {pathname === "/chat" && ( -
+
- + +

Chats

+
diff --git a/apps/web/src/components/screens/login/form.tsx b/apps/web/src/components/screens/login/form.tsx index 0b09889..8fa8413 100644 --- a/apps/web/src/components/screens/login/form.tsx +++ b/apps/web/src/components/screens/login/form.tsx @@ -6,6 +6,8 @@ import { log, toast } from "@tensamin/shared/log"; import { File } from "lucide-react"; import * as React from "react"; import { z } from "zod"; +import { isTauri } from "@tauri-apps/api/core"; +import QrCodeScanner from "@tensamin/tauri/qrCodeScanner"; const fetchedUser = z.object({ id: z.uuidv4(), @@ -145,15 +147,46 @@ export default function Form() { [save], ); + const isTauriEnv = isTauri(); + return (
-
uploadRef.current?.click()} - className="flex flex-col gap-3 cursor-pointer w-55 aspect-square bg-input/13 hover:bg-input/30 transition-all duration-300 ease-in-out border-3 items-center justify-center rounded-lg" - > - -

Select .tu file

-
+ {isTauriEnv ? ( + <> + { + if (!data.startsWith("tensamin://tu::")) { + toast("error", "Invalid QR code"); + return; + } else { + const decoded = data.replace("tensamin://tu::", ""); + + try { + const { userId, privateKey } = parseTuFileContent(decoded); + save("session_id", Date.now()); + save("user_id", userId); + save("private_key", privateKey); + location.href = "/"; + } catch (error) { + log(0, "login", "red", error); + toast("error", "Failed to parse QR code data"); + } + } + }} + /> + + + ) : ( +
uploadRef.current?.click()} + className="flex flex-col gap-3 cursor-pointer w-55 aspect-square bg-input/13 hover:bg-input/30 transition-all duration-300 ease-in-out border-3 items-center justify-center rounded-lg" + > + +

Select .tu file

+
+ )} @@ -42,7 +42,7 @@ export default function Sidebar() {
- {isMobile && location.pathname === "/chat" && ( + {isMobile && !showMobileNavbar && ( diff --git a/apps/web/src/features/legal/screen.tsx b/apps/web/src/features/legal/screen.tsx index 171fab4..9b17c53 100644 --- a/apps/web/src/features/legal/screen.tsx +++ b/apps/web/src/features/legal/screen.tsx @@ -162,7 +162,7 @@ export default function Screen(props: { children: React.ReactNode }) { return ( -
+

Privacy Policy & ToS

diff --git a/apps/web/src/features/settings/components.tsx b/apps/web/src/features/settings/components.tsx new file mode 100644 index 0000000..351bae6 --- /dev/null +++ b/apps/web/src/features/settings/components.tsx @@ -0,0 +1,35 @@ +import { Label } from "@tensamin/ui/cmp/label"; +import { Switch as UISwitch } from "@tensamin/ui/cmp/switch"; +import { useEffect, useState } from "react"; + +import { settingsStorageDefaults } from "@tensamin/shared/settings"; +import { useStorage } from "@tensamin/storage/context"; + +export function Switch({ + label, + id, +}: { + label: string; + id: keyof typeof settingsStorageDefaults; +}) { + const { save, load } = useStorage(); + const [value, setValue] = useState(settingsStorageDefaults[id]); + + useEffect(() => { + load(id).then((value) => setValue(value)); + }, [id, load]); + + return ( +

+ { + setValue(value); + save(id, value); + }} + /> + +
+ ); +} diff --git a/apps/web/src/features/settings/layout.tsx b/apps/web/src/features/settings/layout.tsx new file mode 100644 index 0000000..fb84f4d --- /dev/null +++ b/apps/web/src/features/settings/layout.tsx @@ -0,0 +1,64 @@ +import { Button } from "@tensamin/ui/cmp/button"; + +import options from "@tensamin/shared/settings"; +import { cn, useIsMobile } from "@tensamin/ui/utils"; +import { Outlet, useNavigate } from "@tanstack/react-router"; + +export default function Screen() { + const isMobile = useIsMobile(); + + return ( +
+ {!isMobile && } + + {/* Page */} +
+ +
+
+ ); +} + +export function SettingsSidebar() { + const settingsOptions = options as Record< + string, + Record> + >; + + const isMobile = useIsMobile(); + const navigate = useNavigate(); + + return ( +
+ {/* Settings */} + {Object.keys(settingsOptions).map((category) => ( + // Category +
+

{category}

+ {Object.keys(settingsOptions[category]).map((page) => ( + // Page +
+ +
+ ))} +
+ ))} +
+ ); +} diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index be49329..939c676 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -10,8 +10,9 @@ import "./index.css"; import NotFound from "@/routes/404"; -import RootLayout from "./routes/layout"; -import AppLayout from "./routes/app/layout"; +import RootLayout from "@/routes/layout"; +import AppLayout from "@/routes/app/layout"; +import SettingsLayout from "@/features/settings/layout"; import Home from "@/routes/app/home"; import ChatScreen from "@tensamin/chat/screen"; @@ -22,6 +23,7 @@ import ChatContext from "@tensamin/chat/context"; import CallContext from "@tensamin/call/context"; import SocketContext from "@tensamin/ttp/context"; import UserContext from "@tensamin/user/context"; +import DeeplinkContext from "@tensamin/tauri/deeplinkHandler"; import { ThemeProvider } from "@tensamin/ui/theme"; import z from "zod"; @@ -47,13 +49,15 @@ window.setLogLevelToMax = () => { function RootShell() { return ( - -
- - - -
-
+ + +
+ + + +
+
+
); } @@ -89,10 +93,55 @@ const appRoute = createRoute({ component: AppShell, }); +const settingsRoute = createRoute({ + getParentRoute: () => appRoute, + path: "settings", + component: SettingsLayout, + staticData: { + showMobileNavbar: true, + }, +}); + +type SettingsRouteModule = { + default?: () => React.JSX.Element; + component?: () => React.JSX.Element; +}; + +const settingsRouteModules = import.meta.glob( + "./routes/settings/*.tsx", + { eager: true }, +); + +const settingsChildren = Object.entries(settingsRouteModules).map( + ([filePath, module]) => { + const fileName = filePath.split("/").pop()?.replace(".tsx", "") ?? ""; + const path = fileName === "index" ? "/" : fileName; + const component = module.component ?? module.default; + + if (!component) { + throw new Error( + `Settings route module "${filePath}" must export a default component`, + ); + } + + return createRoute({ + getParentRoute: () => settingsRoute, + path, + component, + staticData: { + showMobileNavbar: true, + }, + }); + }, +); + const homeRoute = createRoute({ getParentRoute: () => appRoute, path: "/", component: Home, + staticData: { + showMobileNavbar: true, + }, }); const chatRoute = createRoute({ @@ -102,22 +151,36 @@ const chatRoute = createRoute({ validateSearch: z.object({ id: z.number().optional(), }), + staticData: { + showMobileNavbar: false, + }, }); const callRoute = createRoute({ getParentRoute: () => appRoute, path: "call", component: CallScreen, + staticData: { + showMobileNavbar: false, + }, }); const loginRoute = createRoute({ getParentRoute: () => rootRoute, path: "login", component: Login, + staticData: { + showMobileNavbar: false, + }, }); const routeTree = rootRoute.addChildren([ - appRoute.addChildren([homeRoute, chatRoute, callRoute]), + appRoute.addChildren([ + homeRoute, + chatRoute, + callRoute, + settingsRoute.addChildren(settingsChildren), + ]), loginRoute, ]); diff --git a/apps/web/src/routes/app/layout.tsx b/apps/web/src/routes/app/layout.tsx index fad88d8..0f6a143 100644 --- a/apps/web/src/routes/app/layout.tsx +++ b/apps/web/src/routes/app/layout.tsx @@ -7,31 +7,31 @@ import { SidebarProvider } from "@tensamin/ui/cmp/sidebar"; import { useIsMobile, cn } from "@tensamin/ui/utils"; import { isTauri } from "@tauri-apps/api/core"; -import { useLocation } from "@tanstack/react-router"; +import { useLocation, useMatches } from "@tanstack/react-router"; /** * Executes Layout. * @param props Parameter props. * @returns unknown. */ -export default function Layout(props: { children: ReactNode }) { +export default function Layout({ children }: { children: ReactNode }) { const isMobile = useIsMobile(); - const location = useLocation(); + const showMobileNavbar = useShowMobileNavbar(); return (
{!isMobile && } - {isMobile && location.pathname === "/chat" && ( - - )} + {isMobile && !showMobileNavbar && }
- {props.children} + {children}
- {isMobile && location.pathname !== "/chat" && } + + {isMobile && showMobileNavbar && }
); } + +export function useShowMobileNavbar(): boolean { + const matches = useMatches(); + const location = useLocation(); + + const value = matches.some( + (match) => + match.pathname === location.pathname && + // @ts-expect-error Stuff + match.staticData?.showMobileNavbar === true, + ); + + return value; +} diff --git a/apps/web/src/routes/settings/chat.tsx b/apps/web/src/routes/settings/chat.tsx new file mode 100644 index 0000000..3dafa45 --- /dev/null +++ b/apps/web/src/routes/settings/chat.tsx @@ -0,0 +1,12 @@ +import { Switch } from "@/features/settings/components"; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/apps/web/src/routes/settings/index.tsx b/apps/web/src/routes/settings/index.tsx new file mode 100644 index 0000000..3ea9a55 --- /dev/null +++ b/apps/web/src/routes/settings/index.tsx @@ -0,0 +1,7 @@ +import { SettingsSidebar } from "@/features/settings/layout"; +import { useIsMobile } from "@tensamin/ui/utils"; + +export default function Page() { + const isMobile = useIsMobile(); + return isMobile && ; +} diff --git a/apps/web/src/routes/settings/security.tsx b/apps/web/src/routes/settings/security.tsx new file mode 100644 index 0000000..f52636f --- /dev/null +++ b/apps/web/src/routes/settings/security.tsx @@ -0,0 +1,111 @@ +import { useStorage } from "@tensamin/storage/context"; +import { useEffect, useState } from "react"; +import QRCode from "qrcode"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@tensamin/ui/cmp/alert-dialog"; +import { Button } from "@tensamin/ui/cmp/button"; + +export default function Page() { + return ( +
+ +
+ ); +} + +// QR Code Login +const generateQR = async (text: string): Promise => { + try { + const url = await QRCode.toDataURL(text, { + errorCorrectionLevel: "H", + margin: 1, + color: { + dark: "#000000FF", + light: "#FFFFFFFF", + }, + }); + return url; + } catch (err) { + console.error(err); + throw err; + } +}; +function QrCodeLogin() { + const { load } = useStorage(); + const [userId, setUserId] = useState(0); + const [privateKey, setPrivateKey] = useState(""); + const [qrCodeBase64, setQrCodeBase64] = useState( + undefined, + ); + + useEffect(() => { + load("private_key").then((value) => { + if (value) { + setPrivateKey(value); + } + }); + load("user_id").then((value) => { + if (value) { + setUserId(value); + } + }); + }, [load]); + + useEffect(() => { + if (userId && privateKey) { + generateQR(`tensamin://tu::${userId}::${privateKey}`).then( + setQrCodeBase64, + ); + } + }, [userId, privateKey]); + + const [qrCodeVisible, setQrCodeVisible] = useState(false); + + return ( +
+ {qrCodeBase64 && ( + QR Code + )} + {!qrCodeVisible && ( + <> +
+
+ + Show QR Code} /> + + + Are you sure? + + This will expose your private key! This is the same as + sharing the .tu file! It's very tedious to change the + private key, so only do this if you are sure no one can + steal it! + + + + Cancel + setQrCodeVisible(true)}> + Show QR Code + + } + /> + + + +
+ + )} +
+ ); +} diff --git a/bun.lock b/bun.lock index 1372191..a8e64c4 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,10 @@ "version": "0.0.0", "dependencies": { "@tauri-apps/api": "^2", + "@tauri-apps/plugin-barcode-scanner": "~2", + "@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-opener": "^2", + "@tensamin/shared": "workspace:*", "@tensamin/ui": "workspace:*", "lucide-react": "^1.7.0", "react": "^19.2.0", @@ -57,6 +60,7 @@ "comlink": "^4.4.2", "framer-motion": "^12.38.0", "lucide-react": "^0.564.0", + "qrcode": "^1.5.4", "react": "^19.2.0", "react-dom": "^19.2.0", "shadcn": "^4.0.7", @@ -70,6 +74,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@types/qrcode": "^1.5.6", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.0.4", @@ -198,6 +203,7 @@ "version": "0.0.0", "dependencies": { "@base-ui/react": "^1.3.0", + "@tauri-apps/api": "^2", "@tensamin/tauri": "workspace:*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -601,6 +607,10 @@ "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="], + "@tauri-apps/plugin-barcode-scanner": ["@tauri-apps/plugin-barcode-scanner@2.4.4", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-uXvyMI8UgQjSrGxzTU5isNoQarMGRxFmTmb4TsgiWZHf/g7LsIyAQCwoFShjax0fXCK5mdVKDOvlkfOr21fo6g=="], + + "@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.8", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-Cd2Cs960MGuGONeIwxOPx9wqwedetAHOGlwK5boJ/SMTfAtAyfErpfVPEn+EJzgXsJun8EKzsEumHjr+64V4fw=="], + "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="], "@tensamin/call": ["@tensamin/call@workspace:packages/call"], @@ -647,6 +657,8 @@ "@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], + "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -721,6 +733,8 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -733,7 +747,7 @@ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -775,6 +789,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], @@ -793,6 +809,8 @@ "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -803,7 +821,7 @@ "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], - "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], @@ -1147,6 +1165,8 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -1171,6 +1191,8 @@ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], @@ -1189,6 +1211,8 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -1209,6 +1233,8 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -1247,6 +1273,8 @@ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "shadcn": ["shadcn@4.0.7", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-QwnD52EtnZo575yG/mk4fp6n0Qu8HZTVj10dsbg1O8DBLZPZG1V7AGOgTQSTjdsl8Jtye8GJfoKFz5npBkfExg=="], @@ -1279,7 +1307,7 @@ "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="], @@ -1381,6 +1409,8 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -1389,13 +1419,13 @@ "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -1451,12 +1481,8 @@ "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "eciesjs/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], "eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -1469,8 +1495,12 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "msw/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -1479,11 +1509,11 @@ "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], @@ -1503,18 +1533,30 @@ "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "msw/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "msw/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "msw/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "msw/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "msw/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "msw/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } } diff --git a/packages/chat/src/components/input.tsx b/packages/chat/src/components/input.tsx index e770122..cb5888a 100644 --- a/packages/chat/src/components/input.tsx +++ b/packages/chat/src/components/input.tsx @@ -10,6 +10,7 @@ import { useTTP } from "@tensamin/ttp/context"; import { useCrypto } from "@tensamin/crypto/context"; import { log, toast } from "@tensamin/shared/log"; import Message from "./message"; +import { cn, useIsMobile } from "@tensamin/ui/utils"; export default function InputComponent({ value, @@ -27,7 +28,7 @@ export default function InputComponent({ const { load } = useStorage(); React.useEffect(() => { - void load("chat_invert_enter_behaviour").then((shouldInvert) => { + void load("settings.reverse_enter_behavior").then((shouldInvert) => { setInvertEnterBehavior(shouldInvert); }); }, [load]); @@ -100,8 +101,17 @@ export default function InputComponent({ setValue(""); } + const isMobile = useIsMobile(); + return ( - + -
+
diff --git a/packages/shared/package.json b/packages/shared/package.json index db6b93d..baeaa0d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -6,6 +6,7 @@ "exports": { "./data": "./src/data.ts", "./log": "./src/log.tsx", + "./settings": "./src/settings.ts", "./features/legal/schema": "./src/features/legal/schema.ts", "./features/conversation/schema": "./src/features/conversation/schema.ts" }, diff --git a/packages/shared/src/data.ts b/packages/shared/src/data.ts index 4a14dbb..7de68ce 100644 --- a/packages/shared/src/data.ts +++ b/packages/shared/src/data.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { legalDocsSchema } from "./features/legal/schema"; +import { settingsStorageDefaults, type SettingsStorageDefaults } from "./settings"; const fileFromMessage = z.object({ name: z.string(), @@ -168,7 +169,7 @@ export const ttp = { export type TTP = typeof ttp; // Storage -export interface Storage { +export interface Storage extends SettingsStorageDefaults { session_id: number; user_id: number; private_key: string; @@ -179,7 +180,6 @@ export interface Storage { analytics_usage_data: boolean; analytics_done: boolean; legal_docs: z.infer; - chat_invert_enter_behaviour: boolean; } export const storageDefaults: Storage = { @@ -192,6 +192,7 @@ export const storageDefaults: Storage = { analytics_crash_reports: true, analytics_usage_data: true, analytics_done: false, + ...settingsStorageDefaults, legal_docs: { eula: { version: "0.0", @@ -209,5 +210,4 @@ export const storageDefaults: Storage = { unix: 0, }, }, - chat_invert_enter_behaviour: false, }; diff --git a/packages/shared/src/settings.ts b/packages/shared/src/settings.ts new file mode 100644 index 0000000..70212d2 --- /dev/null +++ b/packages/shared/src/settings.ts @@ -0,0 +1,76 @@ +type StringKeyOf = Extract; + +type SettingDefinition = { + display: string; + type: string; + default?: unknown; +}; + +export type SettingsSchema = Record< + string, + Record> +>; + +const settings = { + account: { + security: { + login_qr_code: { + display: "Login QR Code", + type: "custom", + }, + }, + }, + accessability: { + chat: { + reverse_enter_behavior: { + display: "Reverse Enter Behavior", + type: "boolean", + default: false, + }, + }, + }, +} as const satisfies SettingsSchema; + +export default settings; + +type BooleanSettingNames = { + [C in StringKeyOf]: { + [P in StringKeyOf]: { + [S in StringKeyOf]: T[C][P][S] extends { + type: "boolean"; + default: boolean; + } + ? S + : never; + }[StringKeyOf]; + }[StringKeyOf]; +}[StringKeyOf]; + +export type SettingsStorageKey = + `settings.${BooleanSettingNames}`; + +export type SettingsStorageDefaults = + { + [K in SettingsStorageKey]: boolean; + }; + +function buildSettingsStorageDefaults( + schema: T, +): SettingsStorageDefaults { + const defaults: Partial> = {}; + + for (const category of Object.values(schema)) { + for (const page of Object.values(category)) { + for (const [settingName, setting] of Object.entries(page)) { + if (setting.type === "boolean" && typeof setting.default === "boolean") { + const key = `settings.${settingName}` as SettingsStorageKey; + defaults[key] = setting.default; + } + } + } + } + + return defaults as SettingsStorageDefaults; +} + +export const settingsStorageDefaults = buildSettingsStorageDefaults(settings); \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index d523bc0..c80665b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -11,12 +11,12 @@ "./utils": "./src/lib/utils.ts" }, "scripts": { - "patch": "find src -type f \\( -name \"*.ts\" -o -name \"*.tsx\" \\) -print0 | xargs -0 sed -i 's|@/lib/utils|../lib/utils|g; s|@/cmp/|./|g' && bunx prettier --write .", "format": "bunx prettier --write .", "lint": "eslint src", "build": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "@tauri-apps/api": "^2", "@tensamin/tauri": "workspace:*", "@base-ui/react": "^1.3.0", "class-variance-authority": "^0.7.1", diff --git a/packages/ui/src/cmp/switch.tsx b/packages/ui/src/cmp/switch.tsx new file mode 100644 index 0000000..88ac5e9 --- /dev/null +++ b/packages/ui/src/cmp/switch.tsx @@ -0,0 +1,30 @@ +import { Switch as SwitchPrimitive } from "@base-ui/react/switch" + +import { cn } from "../lib/utils" + +function Switch({ + className, + size = "default", + ...props +}: SwitchPrimitive.Root.Props & { + size?: "sm" | "default" +}) { + return ( + + + + ) +} + +export { Switch } diff --git a/packages/ui/src/screens/util.tsx b/packages/ui/src/screens/util.tsx index 584296c..1029d57 100644 --- a/packages/ui/src/screens/util.tsx +++ b/packages/ui/src/screens/util.tsx @@ -1,5 +1,6 @@ import { cn } from "../lib/utils"; import Controls from "@tensamin/tauri/controls"; +import { isTauri } from "@tauri-apps/api/core"; export default function CreateScreen({ children, @@ -41,7 +42,13 @@ import { useIsMobile } from "../lib/utils"; export function StorageDestoryer() { const isMobile = useIsMobile(); return ( -
+