wip: migrate to desktop2

This commit is contained in:
reya 2024-02-12 09:08:35 +07:00
parent c809ab6b4e
commit 1950cb59a2
25 changed files with 347 additions and 51 deletions

View File

@ -7,8 +7,7 @@
<title>Lume Desktop</title>
</head>
<body
class="relative w-screen h-screen overflow-hidden font-sans antialiased cursor-default select-none text-neutral-950 dark:text-neutral-50">
<body class="relative w-screen h-screen overflow-hidden font-sans antialiased cursor-default select-none">
<div class="fixed top-0 left-0 z-50 w-full h-9" data-tauri-drag-region></div>
<div id="root"></div>
<script type="module" src="/src/app.tsx"></script>

View File

@ -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:^",

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

View File

@ -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(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
<StorageProvider>
<ArkProvider>
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
</ArkProvider>
</StorageProvider>
</I18nextProvider>,
);
}

23
apps/desktop2/src/i18n.ts Normal file
View File

@ -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;

View File

@ -3,14 +3,12 @@ import {
ScrollRestoration,
createRootRoute,
} from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
export const Route = createRootRoute({
component: () => (
<>
<ScrollRestoration />
<Outlet />
<TanStackRouterDevtools />
</>
),
});

View File

@ -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<Keys>(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 (
<div className="flex flex-col items-center justify-center w-screen h-screen">
<div>
<h3>Backup your key</h3>
<div className="flex flex-col gap-2">
{keys ? <input name="nsec" readOnly value={keys.nsec} /> : null}
<button
type="button"
onClick={submit}
className="w-full h-11 bg-gray-3 hover:bg-gray-4"
>
Submit
</button>
</div>
</div>
</div>
);
}

View File

@ -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 (
<div className="flex flex-col items-center justify-center w-screen h-screen">
<div>
<h3>Import your key</h3>
<div className="flex flex-col gap-2">
<input
name="nsec"
value={key}
onChange={(e) => setKey(e.target.value)}
/>
<button
type="button"
onClick={submit}
className="w-full h-11 bg-gray-3 hover:bg-gray-4"
>
Submit
</button>
</div>
</div>
</div>
);
}

View File

@ -1,13 +0,0 @@
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/")({
component: Index,
});
function Index() {
return (
<div className="p-2">
<h3>Welcome Home!</h3>
</div>
);
}

View File

@ -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 (
<div className="p-2">
<h3>Welcome Home!</h3>
</div>
);
}

View File

@ -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 (
<div className="flex flex-col w-screen h-screen bg-black">
<div className="flex flex-col items-center justify-between w-full h-full">
<div />
<div className="flex flex-col items-center w-full max-w-4xl gap-10 mx-auto">
<div className="flex flex-col items-center text-center">
<img
src={`/heading-${storage.locale}.png`}
srcSet={`/heading-${storage.locale}@2x.png 2x`}
alt="lume"
className="w-2/3"
/>
<p className="mt-5 text-lg font-medium leading-snug whitespace-pre-line text-gray-7">
{t("welcome.title")}
</p>
</div>
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
<Link
to="/auth/create"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-10 rounded-xl hover:bg-blue-11"
>
{t("welcome.signup")}
</Link>
<Link
to="/auth/import"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white rounded-xl bg-gray-12 hover:bg-gray-11"
>
{t("welcome.login")}
</Link>
</div>
</div>
<div className="flex items-center justify-center h-11">
<p className="text-gray-8">
{t("welcome.footer")}{" "}
<Link
to="https://nostr.com"
target="_blank"
className="text-blue-500"
>
here
</Link>
</p>
</div>
</div>
</div>
);
}

View File

@ -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 */

View File

@ -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",

View File

@ -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));

View File

@ -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<Ark>(undefined);
export const ArkProvider = ({ children }: PropsWithChildren<object>) => {
const ark = useMemo(() => new Ark(), []);
useEffect(() => {
if (ark) ark.load_account();
}, []);
return <ArkContext.Provider value={ark}>{children}</ArkContext.Provider>;
};

View File

@ -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);

View File

@ -55,8 +55,7 @@ export interface Metadata {
}
export interface CurrentAccount {
pubkey: string;
npub?: string;
npub: string;
contacts?: string[];
interests?: Interests;
}

View File

@ -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:^

View File

@ -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,

View File

@ -25,7 +25,7 @@ pub fn create_keys() -> Result<CreateKeysResponse, ()> {
}
#[tauri::command]
pub fn save_key(nsec: &str, app_handle: tauri::AppHandle) -> Result<bool, bool> {
pub fn save_key(nsec: &str, app_handle: tauri::AppHandle) -> Result<bool, ()> {
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<bool, bool>
Ok(true)
} else {
Err(false)
Ok(false)
}
}
@ -60,7 +60,12 @@ pub fn save_key(nsec: &str, app_handle: tauri::AppHandle) -> Result<bool, bool>
pub fn get_public_key(nsec: &str) -> Result<String, ()> {
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<String, ()> {
pub async fn verify_signer(nostr: State<'_, Nostr>) -> Result<bool, ()> {
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<String, ()> {
let user = &nostr.client_user;
if let Some(public_key) = user {
Ok(public_key.to_string())
} else {
Err(())
}