feat: migrate frontend to new backend

This commit is contained in:
2024-02-08 21:24:08 +07:00
parent 17052aeeaa
commit ec78cf8bf7
34 changed files with 478 additions and 650 deletions

View File

@@ -1,18 +1,10 @@
import { LoaderIcon } from "@lume/icons";
import { ArkProvider } from "@lume/ark";
import { StorageProvider } from "@lume/storage";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { fetch } from "@tauri-apps/plugin-http";
import { I18nextProvider } from "react-i18next";
import {
RouterProvider,
createBrowserRouter,
defer,
redirect,
} from "react-router-dom";
import { Toaster } from "sonner";
import i18n from "./i18n";
import { ErrorScreen } from "./routes/error";
import Router from "./router";
const queryClient = new QueryClient({
defaultOptions: {
@@ -22,278 +14,15 @@ const queryClient = new QueryClient({
},
});
const router = createBrowserRouter([
{
async lazy() {
const { AppLayout } = await import("@lume/ui");
return { Component: AppLayout };
},
children: [
{
path: "/",
errorElement: <ErrorScreen />,
async lazy() {
const { HomeLayout } = await import("@lume/ui");
return { Component: HomeLayout };
},
loader: async () => {
const signer = await invoke("verify_signer");
if (!signer) return redirect("auth");
return null;
},
children: [
{
index: true,
async lazy() {
const { HomeScreen } = await import("./routes/home");
return { Component: HomeScreen };
},
},
],
},
{
path: "settings",
async lazy() {
const { SettingsLayout } = await import("@lume/ui");
return { Component: SettingsLayout };
},
children: [
{
index: true,
async lazy() {
const { GeneralSettingScreen } = await import(
"./routes/settings/general"
);
return { Component: GeneralSettingScreen };
},
},
{
path: "profile",
async lazy() {
const { ProfileSettingScreen } = await import(
"./routes/settings/profile"
);
return { Component: ProfileSettingScreen };
},
},
{
path: "backup",
async lazy() {
const { BackupSettingScreen } = await import(
"./routes/settings/backup"
);
return { Component: BackupSettingScreen };
},
},
{
path: "advanced",
async lazy() {
const { AdvancedSettingScreen } = await import(
"./routes/settings/advanced"
);
return { Component: AdvancedSettingScreen };
},
},
{
path: "nwc",
async lazy() {
const { NWCScreen } = await import("./routes/settings/nwc");
return { Component: NWCScreen };
},
},
{
path: "about",
async lazy() {
const { AboutScreen } = await import("./routes/settings/about");
return { Component: AboutScreen };
},
},
],
},
{
path: "activity",
async lazy() {
const { ActivityScreen } = await import("./routes/activty");
return { Component: ActivityScreen };
},
children: [
{
path: ":id",
async lazy() {
const { ActivityIdScreen } = await import("./routes/activty/id");
return { Component: ActivityIdScreen };
},
},
],
},
{
path: "relays",
async lazy() {
const { RelaysScreen } = await import("./routes/relays");
return { Component: RelaysScreen };
},
children: [
{
index: true,
async lazy() {
const { RelayGlobalScreen } = await import(
"./routes/relays/global"
);
return { Component: RelayGlobalScreen };
},
},
{
path: "follows",
async lazy() {
const { RelayFollowsScreen } = await import(
"./routes/relays/follows"
);
return { Component: RelayFollowsScreen };
},
},
{
path: ":url",
loader: async ({ request, params }) => {
return defer({
relay: fetch(`https://${params.url}`, {
method: "GET",
headers: {
Accept: "application/nostr+json",
},
signal: request.signal,
}).then((res) => res.json()),
});
},
async lazy() {
const { RelayUrlScreen } = await import("./routes/relays/url");
return { Component: RelayUrlScreen };
},
},
],
},
{
path: "depot",
children: [
{
index: true,
async lazy() {
const { DepotScreen } = await import("./routes/depot");
return { Component: DepotScreen };
},
},
{
path: "onboarding",
async lazy() {
const { DepotOnboardingScreen } = await import(
"./routes/depot/onboarding"
);
return { Component: DepotOnboardingScreen };
},
},
],
},
],
},
{
path: "auth",
errorElement: <ErrorScreen />,
async lazy() {
const { AuthLayout } = await import("@lume/ui");
return { Component: AuthLayout };
},
children: [
{
index: true,
async lazy() {
const { WelcomeScreen } = await import("./routes/auth/welcome");
return { Component: WelcomeScreen };
},
},
{
path: "create",
async lazy() {
const { CreateAccountScreen } = await import("./routes/auth/create");
return { Component: CreateAccountScreen };
},
},
{
path: "create-keys",
async lazy() {
const { CreateAccountKeys } = await import(
"./routes/auth/create-keys"
);
return { Component: CreateAccountKeys };
},
},
{
path: "create-address",
loader: async () => {
// return await ark.getOAuthServices();
return null;
},
async lazy() {
const { CreateAccountAddress } = await import(
"./routes/auth/create-address"
);
return { Component: CreateAccountAddress };
},
},
{
path: "login",
async lazy() {
const { LoginScreen } = await import("./routes/auth/login");
return { Component: LoginScreen };
},
},
{
path: "login-key",
async lazy() {
const { LoginWithKey } = await import("./routes/auth/login-key");
return { Component: LoginWithKey };
},
},
{
path: "login-nsecbunker",
async lazy() {
const { LoginWithNsecbunker } = await import(
"./routes/auth/login-nsecbunker"
);
return { Component: LoginWithNsecbunker };
},
},
{
path: "login-oauth",
async lazy() {
const { LoginWithOAuth } = await import("./routes/auth/login-oauth");
return { Component: LoginWithOAuth };
},
},
{
path: "onboarding",
async lazy() {
const { OnboardingScreen } = await import("./routes/auth/onboarding");
return { Component: OnboardingScreen };
},
},
],
},
]);
export default function App() {
return (
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
<QueryClientProvider client={queryClient}>
<Toaster position="top-center" theme="system" closeButton />
<StorageProvider>
<RouterProvider
router={router}
fallbackElement={
<div className="flex items-center justify-center w-full h-full">
<LoaderIcon className="w-6 h-6 animate-spin" />
</div>
}
future={{ v7_startTransition: true }}
/>
<ArkProvider>
<Router />
</ArkProvider>
</StorageProvider>
</QueryClientProvider>
</I18nextProvider>

290
apps/desktop/src/router.tsx Normal file
View File

@@ -0,0 +1,290 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import {
RouterProvider,
createBrowserRouter,
defer,
redirect,
} from "react-router-dom";
import { ErrorScreen } from "./routes/error";
export default function Router() {
const ark = useArk();
const router = createBrowserRouter([
{
async lazy() {
const { AppLayout } = await import("@lume/ui");
return { Component: AppLayout };
},
children: [
{
path: "/",
errorElement: <ErrorScreen />,
async lazy() {
const { HomeLayout } = await import("@lume/ui");
return { Component: HomeLayout };
},
loader: async () => {
const signer = await ark.verify_signer();
if (!signer) return redirect("auth");
return null;
},
children: [
{
index: true,
async lazy() {
const { HomeScreen } = await import("./routes/home");
return { Component: HomeScreen };
},
},
],
},
{
path: "settings",
async lazy() {
const { SettingsLayout } = await import("@lume/ui");
return { Component: SettingsLayout };
},
children: [
{
index: true,
async lazy() {
const { GeneralSettingScreen } = await import(
"./routes/settings/general"
);
return { Component: GeneralSettingScreen };
},
},
{
path: "profile",
async lazy() {
const { ProfileSettingScreen } = await import(
"./routes/settings/profile"
);
return { Component: ProfileSettingScreen };
},
},
{
path: "backup",
async lazy() {
const { BackupSettingScreen } = await import(
"./routes/settings/backup"
);
return { Component: BackupSettingScreen };
},
},
{
path: "advanced",
async lazy() {
const { AdvancedSettingScreen } = await import(
"./routes/settings/advanced"
);
return { Component: AdvancedSettingScreen };
},
},
{
path: "nwc",
async lazy() {
const { NWCScreen } = await import("./routes/settings/nwc");
return { Component: NWCScreen };
},
},
{
path: "about",
async lazy() {
const { AboutScreen } = await import("./routes/settings/about");
return { Component: AboutScreen };
},
},
],
},
{
path: "activity",
async lazy() {
const { ActivityScreen } = await import("./routes/activty");
return { Component: ActivityScreen };
},
children: [
{
path: ":id",
async lazy() {
const { ActivityIdScreen } = await import(
"./routes/activty/id"
);
return { Component: ActivityIdScreen };
},
},
],
},
{
path: "relays",
async lazy() {
const { RelaysScreen } = await import("./routes/relays");
return { Component: RelaysScreen };
},
children: [
{
index: true,
async lazy() {
const { RelayGlobalScreen } = await import(
"./routes/relays/global"
);
return { Component: RelayGlobalScreen };
},
},
{
path: "follows",
async lazy() {
const { RelayFollowsScreen } = await import(
"./routes/relays/follows"
);
return { Component: RelayFollowsScreen };
},
},
{
path: ":url",
loader: async ({ request, params }) => {
return defer({
relay: fetch(`https://${params.url}`, {
method: "GET",
headers: {
Accept: "application/nostr+json",
},
signal: request.signal,
}).then((res) => res.json()),
});
},
async lazy() {
const { RelayUrlScreen } = await import("./routes/relays/url");
return { Component: RelayUrlScreen };
},
},
],
},
{
path: "depot",
children: [
{
index: true,
async lazy() {
const { DepotScreen } = await import("./routes/depot");
return { Component: DepotScreen };
},
},
{
path: "onboarding",
async lazy() {
const { DepotOnboardingScreen } = await import(
"./routes/depot/onboarding"
);
return { Component: DepotOnboardingScreen };
},
},
],
},
],
},
{
path: "auth",
errorElement: <ErrorScreen />,
async lazy() {
const { AuthLayout } = await import("@lume/ui");
return { Component: AuthLayout };
},
children: [
{
index: true,
async lazy() {
const { WelcomeScreen } = await import("./routes/auth/welcome");
return { Component: WelcomeScreen };
},
},
{
path: "create",
async lazy() {
const { CreateAccountScreen } = await import(
"./routes/auth/create"
);
return { Component: CreateAccountScreen };
},
},
{
path: "create-keys",
async lazy() {
const { CreateAccountKeys } = await import(
"./routes/auth/create-keys"
);
return { Component: CreateAccountKeys };
},
},
{
path: "create-address",
loader: async () => {
// return await ark.getOAuthServices();
return null;
},
async lazy() {
const { CreateAccountAddress } = await import(
"./routes/auth/create-address"
);
return { Component: CreateAccountAddress };
},
},
{
path: "login",
async lazy() {
const { LoginScreen } = await import("./routes/auth/login");
return { Component: LoginScreen };
},
},
{
path: "login-key",
async lazy() {
const { LoginWithKey } = await import("./routes/auth/login-key");
return { Component: LoginWithKey };
},
},
{
path: "login-nsecbunker",
async lazy() {
const { LoginWithNsecbunker } = await import(
"./routes/auth/login-nsecbunker"
);
return { Component: LoginWithNsecbunker };
},
},
{
path: "login-oauth",
async lazy() {
const { LoginWithOAuth } = await import(
"./routes/auth/login-oauth"
);
return { Component: LoginWithOAuth };
},
},
{
path: "onboarding",
async lazy() {
const { OnboardingScreen } = await import(
"./routes/auth/onboarding"
);
return { Component: OnboardingScreen };
},
},
],
},
]);
return (
<RouterProvider
router={router}
fallbackElement={
<div className="flex items-center justify-center w-full h-full">
<LoaderIcon className="w-6 h-6 animate-spin" />
</div>
}
future={{ v7_startTransition: true }}
/>
);
}

View File

@@ -28,7 +28,12 @@ export function CreateAccountKeys() {
setLoading(true);
// trigger save key
await invoke("save_key", { nsec: key });
const save = await invoke("save_key", { nsec: key });
if (!save) {
setLoading(false);
toast.error("Save account keys failed, please try again later.");
}
// update state
setLoading(false);

View File

@@ -1,7 +1,6 @@
import { useArk } from "@lume/ark";
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { getPublicKey, nip19 } from "nostr-tools";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Trans, useTranslation } from "react-i18next";
@@ -9,7 +8,6 @@ import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
export function LoginWithKey() {
const ark = useArk();
const storage = useStorage();
const navigate = useNavigate();
@@ -31,15 +29,15 @@ export function LoginWithKey() {
setLoading(true);
const privkey = nip19.decode(data.nsec).data as string;
const pubkey = getPublicKey(privkey);
// trigger save key
const save = await invoke("save_key", { nsec: data.nsec });
const account = await storage.createAccount({
pubkey: pubkey,
privkey: privkey,
});
ark.account = account;
if (!save) {
setLoading(false);
toast.error("Save account keys failed, please try again later.");
}
// redirect to next step
return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setLoading(false);

View File

@@ -69,7 +69,7 @@ export function OnboardingScreen() {
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
// get other settings
const data = await storage.getAllSettings();
const data = await storage.settings();
for (const item of data) {
if (item.key === "lowPower")
setSettings((prev) => ({

View File

@@ -1,201 +1,9 @@
import { Antenas } from "@columns/antenas";
import { Default } from "@columns/default";
import { ForYou } from "@columns/foryou";
import { Global } from "@columns/global";
import { Group } from "@columns/group";
import { Hashtag } from "@columns/hashtag";
import { Thread } from "@columns/thread";
import { Timeline } from "@columns/timeline";
import { TrendingNotes } from "@columns/trending-notes";
import { User } from "@columns/user";
import { Waifu } from "@columns/waifu";
import { useColumnContext } from "@lume/ark";
import {
ArrowLeftIcon,
ArrowRightIcon,
PlusIcon,
PlusSquareIcon,
} from "@lume/icons";
import { IColumn } from "@lume/types";
import { TutorialModal } from "@lume/ui/src/tutorial/modal";
import { COL_TYPES } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { VList } from "virtua";
export function HomeScreen() {
const { t } = useTranslation();
const { columns, vlistRef, addColumn } = useColumnContext();
const [selectedIndex, setSelectedIndex] = useState(-1);
const renderItem = (column: IColumn) => {
switch (column.kind) {
case COL_TYPES.default:
return <Default key={column.id} column={column} />;
case COL_TYPES.newsfeed:
return <Timeline key={column.id} column={column} />;
case COL_TYPES.foryou:
return <ForYou key={column.id} column={column} />;
case COL_TYPES.thread:
return <Thread key={column.id} column={column} />;
case COL_TYPES.user:
return <User key={column.id} column={column} />;
case COL_TYPES.hashtag:
return <Hashtag key={column.id} column={column} />;
case COL_TYPES.group:
return <Group key={column.id} column={column} />;
case COL_TYPES.antenas:
return <Antenas key={column.id} column={column} />;
case COL_TYPES.global:
return <Global key={column.id} column={column} />;
case COL_TYPES.trendingNotes:
return <TrendingNotes key={column.id} column={column} />;
case COL_TYPES.waifu:
return <Waifu key={column.id} column={column} />;
default:
return <Default key={column.id} column={column} />;
}
};
return (
<div className="relative w-full h-full">
<VList
ref={vlistRef}
className="h-full w-full flex-nowrap overflow-x-auto !overflow-y-hidden scrollbar-none focus:outline-none"
itemSize={420}
tabIndex={0}
horizontal
onKeyDown={(e) => {
if (!vlistRef.current) return;
switch (e.code) {
case "ArrowUp":
case "ArrowLeft": {
e.preventDefault();
const prevIndex = Math.max(selectedIndex - 1, 0);
setSelectedIndex(prevIndex);
vlistRef.current.scrollToIndex(prevIndex, {
align: "center",
smooth: true,
});
break;
}
case "ArrowDown":
case "ArrowRight": {
e.preventDefault();
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
setSelectedIndex(nextIndex);
vlistRef.current.scrollToIndex(nextIndex, {
align: "center",
smooth: true,
});
break;
}
default:
break;
}
}}
>
{columns.map((column) => renderItem(column))}
<div className="w-[420px] h-full flex items-center justify-center">
<button
type="button"
onClick={async () =>
await addColumn({
kind: COL_TYPES.default,
title: "",
content: "",
})
}
className="size-16 inline-flex items-center justify-center hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-2xl"
>
<PlusIcon className="size-6" />
</button>
</div>
</VList>
<Tooltip.Provider>
<div className="absolute bottom-3 right-3">
<div className="flex items-center gap-1 p-1 bg-black/50 dark:bg-white/30 backdrop-blur-xl rounded-xl shadow-toolbar">
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => {
const prevIndex = Math.max(selectedIndex - 1, 0);
setSelectedIndex(prevIndex);
vlistRef.current.scrollToIndex(prevIndex, {
align: "center",
smooth: true,
});
}}
className="inline-flex items-center justify-center rounded-lg text-white/70 hover:text-white hover:bg-black/30 size-10"
>
<ArrowLeftIcon className="size-5" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
{t("global.moveLeft")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => {
const nextIndex = Math.min(
selectedIndex + 1,
columns.length - 1,
);
setSelectedIndex(nextIndex);
vlistRef.current.scrollToIndex(nextIndex, {
align: "center",
smooth: true,
});
}}
className="inline-flex items-center justify-center rounded-lg text-white/70 hover:text-white hover:bg-black/30 size-10"
>
<ArrowRightIcon className="size-5" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
{t("global.moveRight")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild>
<button
type="button"
onClick={async () =>
await addColumn({
kind: COL_TYPES.default,
title: "",
content: "",
})
}
className="inline-flex items-center justify-center rounded-lg text-white/70 hover:text-white hover:bg-black/30 size-10"
>
<PlusSquareIcon className="size-5" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
{t("global.newColumn")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
<div className="w-px h-6 bg-white/10" />
<TutorialModal />
</div>
</div>
</Tooltip.Provider>
<Timeline column={{ id: 1, kind: 1, title: "", content: "" }} />
</div>
);
}