diff --git a/apps/desktop2/index.html b/apps/desktop2/index.html index 2e9a0363..ea08f587 100644 --- a/apps/desktop2/index.html +++ b/apps/desktop2/index.html @@ -7,8 +7,7 @@ Lume Desktop - +
diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index 525a8a82..59d38dc0 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -9,9 +9,14 @@ "preview": "vite preview" }, "dependencies": { + "@lume/ark": "workspace:^", + "@lume/storage": "workspace:^", "@tanstack/react-router": "^1.16.0", + "i18next": "^23.8.2", + "i18next-resources-to-backend": "^1.2.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-i18next": "^14.0.2" }, "devDependencies": { "@lume/tailwindcss": "workspace:^", diff --git a/apps/desktop2/public/heading-en.png b/apps/desktop2/public/heading-en.png new file mode 100644 index 00000000..fb740945 Binary files /dev/null and b/apps/desktop2/public/heading-en.png differ diff --git a/apps/desktop2/public/heading-en@2x.png b/apps/desktop2/public/heading-en@2x.png new file mode 100644 index 00000000..802c679b Binary files /dev/null and b/apps/desktop2/public/heading-en@2x.png differ diff --git a/apps/desktop2/public/heading-fr.png b/apps/desktop2/public/heading-fr.png new file mode 100644 index 00000000..9a1cf82a Binary files /dev/null and b/apps/desktop2/public/heading-fr.png differ diff --git a/apps/desktop2/public/heading-fr@2x.png b/apps/desktop2/public/heading-fr@2x.png new file mode 100644 index 00000000..16bf43fd Binary files /dev/null and b/apps/desktop2/public/heading-fr@2x.png differ diff --git a/apps/desktop2/public/heading-ja.png b/apps/desktop2/public/heading-ja.png new file mode 100644 index 00000000..19134a08 Binary files /dev/null and b/apps/desktop2/public/heading-ja.png differ diff --git a/apps/desktop2/public/heading-ja@2x.png b/apps/desktop2/public/heading-ja@2x.png new file mode 100644 index 00000000..606cb59c Binary files /dev/null and b/apps/desktop2/public/heading-ja@2x.png differ diff --git a/apps/desktop2/src/app.tsx b/apps/desktop2/src/app.tsx index 4b61b627..50a735ae 100644 --- a/apps/desktop2/src/app.tsx +++ b/apps/desktop2/src/app.tsx @@ -1,6 +1,11 @@ +import { ArkProvider } from "@lume/ark"; +import { StorageProvider } from "@lume/storage"; import { RouterProvider, createRouter } from "@tanstack/react-router"; import React, { StrictMode } from "react"; import ReactDOM from "react-dom/client"; +import { I18nextProvider } from "react-i18next"; +import "./app.css"; +import i18n from "./i18n"; // Import the generated route tree import { routeTree } from "./tree.gen"; @@ -21,8 +26,14 @@ const rootElement = document.getElementById("root")!; if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( - - - , + + + + + + + + + , ); } diff --git a/apps/desktop2/src/i18n.ts b/apps/desktop2/src/i18n.ts new file mode 100644 index 00000000..fb75d366 --- /dev/null +++ b/apps/desktop2/src/i18n.ts @@ -0,0 +1,23 @@ +import { resolveResource } from "@tauri-apps/api/path"; +import { readTextFile } from "@tauri-apps/plugin-fs"; +import i18n from "i18next"; +import resourcesToBackend from "i18next-resources-to-backend"; +import { initReactI18next } from "react-i18next"; + +i18n + .use( + resourcesToBackend(async (language: string) => { + const file_path = await resolveResource(`locales/${language}.json`); + return JSON.parse(await readTextFile(file_path)); + }), + ) + .use(initReactI18next) + .init({ + lng: "en", + fallbackLng: "en", + interpolation: { + escapeValue: false, + }, + }); + +export default i18n; diff --git a/apps/desktop2/src/routes/__root.tsx b/apps/desktop2/src/routes/__root.tsx index b02bf17b..b88ac49e 100644 --- a/apps/desktop2/src/routes/__root.tsx +++ b/apps/desktop2/src/routes/__root.tsx @@ -3,14 +3,12 @@ import { ScrollRestoration, createRootRoute, } from "@tanstack/react-router"; -import { TanStackRouterDevtools } from "@tanstack/router-devtools"; export const Route = createRootRoute({ component: () => ( <> - ), }); diff --git a/apps/desktop2/src/routes/auth/create.lazy.tsx b/apps/desktop2/src/routes/auth/create.lazy.tsx new file mode 100644 index 00000000..e7478b94 --- /dev/null +++ b/apps/desktop2/src/routes/auth/create.lazy.tsx @@ -0,0 +1,52 @@ +import { useArk } from "@lume/ark"; +import { Keys } from "@lume/types"; +import { createLazyFileRoute, useNavigate } from "@tanstack/react-router"; +import { invoke } from "@tauri-apps/api/core"; +import { useEffect, useState } from "react"; + +export const Route = createLazyFileRoute("/auth/create")({ + component: Create, +}); + +function Create() { + const ark = useArk(); + const navigate = useNavigate(); + + const [keys, setKeys] = useState(null); + + const submit = async () => { + const save = await ark.save_account(keys); + if (save) { + navigate({ to: "/" }); + } else { + console.log("create failed"); + } + }; + + useEffect(() => { + async function genKeys() { + const cmd: Keys = await invoke("create_keys"); + setKeys(cmd); + } + + genKeys(); + }, []); + + return ( +
+
+

Backup your key

+
+ {keys ? : null} + +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/auth/import.lazy.tsx b/apps/desktop2/src/routes/auth/import.lazy.tsx new file mode 100644 index 00000000..2556b95c --- /dev/null +++ b/apps/desktop2/src/routes/auth/import.lazy.tsx @@ -0,0 +1,55 @@ +import { useArk } from "@lume/ark"; +import { createLazyFileRoute, useNavigate } from "@tanstack/react-router"; +import { invoke } from "@tauri-apps/api/core"; +import { useState } from "react"; + +export const Route = createLazyFileRoute("/auth/import")({ + component: Import, +}); + +function Import() { + const ark = useArk(); + const navigate = useNavigate(); + + const [key, setKey] = useState(""); + + const submit = async () => { + if (!key.startsWith("nsec1")) return; + if (key.length < 30) return; + + const npub: string = await invoke("get_public_key", { nsec: key }); + const keys = { + npub, + nsec: key, + }; + + const save = await ark.save_account(keys); + if (save) { + navigate({ to: "/" }); + } else { + console.log("import failed"); + } + }; + + return ( +
+
+

Import your key

+
+ setKey(e.target.value)} + /> + +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/index.lazy.tsx b/apps/desktop2/src/routes/index.lazy.tsx deleted file mode 100644 index a5bee37f..00000000 --- a/apps/desktop2/src/routes/index.lazy.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createLazyFileRoute } from "@tanstack/react-router"; - -export const Route = createLazyFileRoute("/")({ - component: Index, -}); - -function Index() { - return ( -
-

Welcome Home!

-
- ); -} diff --git a/apps/desktop2/src/routes/index.tsx b/apps/desktop2/src/routes/index.tsx new file mode 100644 index 00000000..d3370bd6 --- /dev/null +++ b/apps/desktop2/src/routes/index.tsx @@ -0,0 +1,25 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { invoke } from "@tauri-apps/api/core"; + +export const Route = createFileRoute("/")({ + component: Index, + beforeLoad: async ({ location }) => { + const signer = await invoke("verify_signer"); + if (!signer) { + throw redirect({ + to: "/landing", + search: { + redirect: location.href, + }, + }); + } + }, +}); + +function Index() { + return ( +
+

Welcome Home!

+
+ ); +} diff --git a/apps/desktop2/src/routes/landing/index.tsx b/apps/desktop2/src/routes/landing/index.tsx new file mode 100644 index 00000000..3fc0edc2 --- /dev/null +++ b/apps/desktop2/src/routes/landing/index.tsx @@ -0,0 +1,59 @@ +import { useStorage } from "@lume/storage"; +import { Link, createFileRoute } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; + +export const Route = createFileRoute("/landing/")({ + component: Index, +}); + +function Index() { + const storage = useStorage(); + const { t } = useTranslation(); + + return ( +
+
+
+
+
+ lume +

+ {t("welcome.title")} +

+
+
+ + {t("welcome.signup")} + + + {t("welcome.login")} + +
+
+
+

+ {t("welcome.footer")}{" "} + + here + +

+
+
+
+ ); +} diff --git a/apps/desktop2/src/tree.gen.ts b/apps/desktop2/src/tree.gen.ts index 01afbeea..4fa60d03 100644 --- a/apps/desktop2/src/tree.gen.ts +++ b/apps/desktop2/src/tree.gen.ts @@ -13,24 +13,54 @@ import { createFileRoute } from '@tanstack/react-router' // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as IndexImport } from './routes/index' +import { Route as LandingIndexImport } from './routes/landing/index' // Create Virtual Routes -const IndexLazyImport = createFileRoute('/')() +const AuthImportLazyImport = createFileRoute('/auth/import')() +const AuthCreateLazyImport = createFileRoute('/auth/create')() // Create/Update Routes -const IndexLazyRoute = IndexLazyImport.update({ +const IndexRoute = IndexImport.update({ path: '/', getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) +} as any) + +const LandingIndexRoute = LandingIndexImport.update({ + path: '/landing/', + getParentRoute: () => rootRoute, +} as any) + +const AuthImportLazyRoute = AuthImportLazyImport.update({ + path: '/auth/import', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/auth/import.lazy').then((d) => d.Route)) + +const AuthCreateLazyRoute = AuthCreateLazyImport.update({ + path: '/auth/create', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/auth/create.lazy').then((d) => d.Route)) // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { interface FileRoutesByPath { '/': { - preLoaderRoute: typeof IndexLazyImport + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/auth/create': { + preLoaderRoute: typeof AuthCreateLazyImport + parentRoute: typeof rootRoute + } + '/auth/import': { + preLoaderRoute: typeof AuthImportLazyImport + parentRoute: typeof rootRoute + } + '/landing/': { + preLoaderRoute: typeof LandingIndexImport parentRoute: typeof rootRoute } } @@ -38,6 +68,11 @@ declare module '@tanstack/react-router' { // Create and export the route tree -export const routeTree = rootRoute.addChildren([IndexLazyRoute]) +export const routeTree = rootRoute.addChildren([ + IndexRoute, + AuthCreateLazyRoute, + AuthImportLazyRoute, + LandingIndexRoute, +]) /* prettier-ignore-end */ diff --git a/apps/desktop2/vite.config.ts b/apps/desktop2/vite.config.ts index 5cd0f888..04c584f9 100644 --- a/apps/desktop2/vite.config.ts +++ b/apps/desktop2/vite.config.ts @@ -12,12 +12,7 @@ export default defineConfig({ promiseExportName: "__tla", promiseImportName: (i) => `__tla_${i}`, }), - TanStackRouterVite({ - routesDirectory: "./src/routes", - generatedRouteTree: "./src/tree.gen.ts", - routeFileIgnorePrefix: "-", - quoteStyle: "single", - }), + TanStackRouterVite(), ], build: { outDir: "../../dist", diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 3d75da4b..8ab44aa7 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -1,20 +1,35 @@ -import { type CurrentAccount, Event, Metadata } from "@lume/types"; +import { type CurrentAccount, Event, Keys, Metadata } from "@lume/types"; import { invoke } from "@tauri-apps/api/core"; export class Ark { public account: CurrentAccount; constructor() { - this.account = { pubkey: "" }; + this.account = { npub: "" }; } - public async verify_signer() { + public async load_account() { try { - const cmd: string = await invoke("verify_signer"); + const cmd: string = await invoke("load_account"); if (cmd) { - this.account.pubkey = cmd; + this.account.npub = cmd; + } + } catch (e) { + console.error(String(e)); + } + } + + public async save_account(keys: Keys) { + try { + const cmd: boolean = await invoke("save_key", { nsec: keys.nsec }); + + if (cmd) { + await invoke("update_signer", { nsec: keys.nsec }); + this.account.npub = keys.npub; + return true; } + return false; } catch (e) { console.error(String(e)); diff --git a/packages/ark/src/provider.tsx b/packages/ark/src/provider.tsx index e0cee303..450c7eb9 100644 --- a/packages/ark/src/provider.tsx +++ b/packages/ark/src/provider.tsx @@ -1,10 +1,21 @@ -import { PropsWithChildren, createContext, useContext, useMemo } from "react"; +import { + PropsWithChildren, + createContext, + useContext, + useEffect, + useMemo, +} from "react"; import { Ark } from "./ark"; export const ArkContext = createContext(undefined); export const ArkProvider = ({ children }: PropsWithChildren) => { const ark = useMemo(() => new Ark(), []); + + useEffect(() => { + if (ark) ark.load_account(); + }, []); + return {children}; }; diff --git a/packages/storage/src/provider.tsx b/packages/storage/src/provider.tsx index 7ad9cdc0..8d598c4f 100644 --- a/packages/storage/src/provider.tsx +++ b/packages/storage/src/provider.tsx @@ -13,7 +13,7 @@ import { type VListHandle } from "virtua"; import { LumeStorage } from "./storage"; const platformName = await platform(); -const osLocale = await locale(); +const osLocale = (await locale()).slice(0, 2); const store = new Store("lume.dat"); const storage = new LumeStorage(store, platformName, osLocale); diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 1375347e..d9b84e71 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -55,8 +55,7 @@ export interface Metadata { } export interface CurrentAccount { - pubkey: string; - npub?: string; + npub: string; contacts?: string[]; interests?: Interests; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71836d8c..cbd8b2ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,15 +256,30 @@ importers: apps/desktop2: dependencies: + '@lume/ark': + specifier: workspace:^ + version: link:../../packages/ark + '@lume/storage': + specifier: workspace:^ + version: link:../../packages/storage '@tanstack/react-router': specifier: ^1.16.0 version: 1.16.0(react-dom@18.2.0)(react@18.2.0) + i18next: + specifier: ^23.8.2 + version: 23.8.2 + i18next-resources-to-backend: + specifier: ^1.2.0 + version: 1.2.0 react: specifier: ^18.2.0 version: 18.2.0 react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-i18next: + specifier: ^14.0.2 + version: 14.0.2(i18next@23.8.2)(react-dom@18.2.0)(react@18.2.0) devDependencies: '@lume/tailwindcss': specifier: workspace:^ diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 266221ab..9e109eb2 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -149,6 +149,7 @@ fn main() { nostr::keys::get_public_key, nostr::keys::update_signer, nostr::keys::verify_signer, + nostr::keys::load_account, nostr::keys::event_to_bech32, nostr::keys::user_to_bech32, nostr::metadata::get_profile, diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index cf4a56c3..cd498e69 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -25,7 +25,7 @@ pub fn create_keys() -> Result { } #[tauri::command] -pub fn save_key(nsec: &str, app_handle: tauri::AppHandle) -> Result { +pub fn save_key(nsec: &str, app_handle: tauri::AppHandle) -> Result { if let Ok(nostr_secret_key) = SecretKey::from_bech32(nsec) { let nostr_keys = Keys::new(nostr_secret_key); let nostr_npub = nostr_keys.public_key().to_bech32().unwrap(); @@ -52,7 +52,7 @@ pub fn save_key(nsec: &str, app_handle: tauri::AppHandle) -> Result Ok(true) } else { - Err(false) + Ok(false) } } @@ -60,7 +60,12 @@ pub fn save_key(nsec: &str, app_handle: tauri::AppHandle) -> Result pub fn get_public_key(nsec: &str) -> Result { let secret_key = SecretKey::from_bech32(nsec).unwrap(); let keys = Keys::new(secret_key); - Ok(keys.public_key().to_bech32().expect("secret key failed")) + Ok( + keys + .public_key() + .to_bech32() + .expect("get public key failed"), + ) } #[tauri::command] @@ -76,16 +81,22 @@ pub async fn update_signer(nsec: &str, nostr: State<'_, Nostr>) -> Result<(), () } #[tauri::command] -pub async fn verify_signer(nostr: State<'_, Nostr>) -> Result { +pub async fn verify_signer(nostr: State<'_, Nostr>) -> Result { let client = &nostr.client; - let user = &nostr.client_user; if let Ok(_) = client.signer().await { - if let Some(public_key) = user { - Ok(public_key.to_string()) - } else { - Err(()) - } + Ok(true) + } else { + Ok(false) + } +} + +#[tauri::command] +pub fn load_account(nostr: State<'_, Nostr>) -> Result { + let user = &nostr.client_user; + + if let Some(public_key) = user { + Ok(public_key.to_string()) } else { Err(()) }