feat: migrate frontend to new backend

This commit is contained in:
reya 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>
);
}

View File

@ -5,7 +5,18 @@ export class Ark {
public account: CurrentAccount;
constructor() {
this.account = null;
this.account = { pubkey: "" };
}
public async verify_signer() {
try {
const cmd: string = await invoke("verify_signer");
if (!cmd) return false;
this.account.pubkey = cmd;
return true;
} catch (e) {
console.error(String(e));
}
}
public async event_to_bech32(id: string, relays: string[]) {
@ -15,8 +26,8 @@ export class Ark {
relays,
});
return cmd;
} catch {
console.error("get nevent id failed");
} catch (e) {
console.error(String(e));
}
}
@ -26,7 +37,70 @@ export class Ark {
const event = JSON.parse(cmd) as Event;
return event;
} catch (e) {
console.error("failed to get event", id);
console.error(String(e));
}
}
public async get_text_events(limit: number, until?: number) {
try {
const cmd: Event[] = await invoke("get_text_events", { limit, until });
return cmd;
} catch (e) {
console.error(String(e));
}
}
public async publish(content: string) {
try {
const cmd: string = await invoke("publish", { content });
return cmd;
} catch (e) {
console.error(String(e));
}
}
public async reply_to(content: string, tags: string[]) {
try {
const cmd: string = await invoke("reply_to", { content, tags });
return cmd;
} catch (e) {
console.error(String(e));
}
}
public async repost(id: string, pubkey: string) {
try {
const cmd: string = await invoke("repost", { id, pubkey });
return cmd;
} catch (e) {
console.error(String(e));
}
}
public async upvote(id: string, pubkey: string) {
try {
const cmd: string = await invoke("upvote", { id, pubkey });
return cmd;
} catch (e) {
console.error(String(e));
}
}
public async downvote(id: string, pubkey: string) {
try {
const cmd: string = await invoke("downvote", { id, pubkey });
return cmd;
} catch (e) {
console.error(String(e));
}
}
public async get_event_thread(id: string) {
try {
const cmd: Event[] = await invoke("get_event_thread", { id });
return cmd;
} catch (e) {
console.error(String(e));
}
}
@ -72,7 +146,7 @@ export class Ark {
const cmd: Metadata = await invoke("get_profile", { id });
return cmd;
} catch (e) {
console.error("failed to get profile", id);
console.error(String(e));
}
}
@ -83,8 +157,8 @@ export class Ark {
relays,
});
return cmd;
} catch {
console.error("get nprofile id failed");
} catch (e) {
console.error(String(e));
}
}
}

View File

@ -1,15 +1,11 @@
import { PinIcon } from "@lume/icons";
import { COL_TYPES } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useTranslation } from "react-i18next";
import { useColumnContext } from "../../column/provider";
import { useNoteContext } from "../provider";
export function NotePin() {
const event = useNoteContext();
const { t } = useTranslation();
const { addColumn } = useColumnContext();
return (
<Tooltip.Provider>
@ -17,13 +13,6 @@ export function NotePin() {
<Tooltip.Trigger asChild>
<button
type="button"
onClick={async () =>
await addColumn({
kind: COL_TYPES.thread,
title: "Thread",
content: event.id,
})
}
className="inline-flex items-center justify-center gap-2 pl-2 pr-3 text-sm font-medium rounded-full h-7 w-max bg-neutral-100 hover:bg-neutral-200 dark:hover:bg-neutral-800 dark:bg-neutral-900"
>
<PinIcon className="size-4" />

View File

@ -1,3 +1,4 @@
import { useStorage } from "@lume/storage";
import { Kind } from "@lume/types";
import {
AUDIOS,
@ -31,6 +32,7 @@ export function NoteContent({
}: {
className?: string;
}) {
const storage = useStorage();
const event = useNoteContext();
const [content, setContent] = useState(event.content);
@ -221,7 +223,7 @@ export function NoteContent({
}
};
if (event.kind !== NDKKind.Text) {
if (event.kind !== Kind.Text) {
return <NIP89 className={className} />;
}

View File

@ -2,17 +2,11 @@ import { COL_TYPES } from "@lume/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useArk } from "../../../hooks/useArk";
import { useProfile } from "../../../hooks/useProfile";
import { useColumnContext } from "../../column/provider";
export function MentionUser({ pubkey }: { pubkey: string }) {
const ark = useArk();
const cleanPubkey = ark.getCleanPubkey(pubkey);
const { isLoading, isError, user } = useProfile(pubkey);
const { t } = useTranslation();
const { addColumn } = useColumnContext();
return (
<DropdownMenu.Root>
@ -21,12 +15,12 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
? "@anon"
: isError
? pubkey
: `@${user?.name || user?.displayName || user?.username || "anon"}`}
: `@${user?.name || user?.display_name || user?.name || "anon"}`}
</DropdownMenu.Trigger>
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none">
<DropdownMenu.Item asChild>
<Link
to={`/users/${cleanPubkey}`}
to={`/users/${pubkey}`}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
{t("note.buttons.viewProfile")}
@ -35,13 +29,6 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
<DropdownMenu.Item asChild>
<button
type="button"
onClick={async () =>
await addColumn({
kind: COL_TYPES.user,
title: user?.name || user?.displayName || "User",
content: cleanPubkey,
})
}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
{t("note.buttons.pin")}

View File

@ -5,7 +5,6 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { useArk } from "../../hooks/useArk";
import { useColumnContext } from "../column/provider";
import { useNoteContext } from "./provider";
export function NoteMenu() {
@ -14,7 +13,6 @@ export function NoteMenu() {
const navigate = useNavigate();
const { t } = useTranslation();
const { addColumn } = useColumnContext();
const copyID = async () => {
await writeText(await ark.event_to_bech32(event.id, [""]));
@ -93,13 +91,6 @@ export function NoteMenu() {
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() =>
addColumn({
kind: COL_TYPES.user,
title: "User",
content: event.pubkey,
})
}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
{t("note.menu.pinAuthor")}

View File

@ -1,9 +1,7 @@
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { Event } from "@lume/types";
import { Note } from "..";
export function ChildReply({
event,
}: { event: NDKEvent; rootEventId?: string }) {
export function ChildReply({ event }: { event: Event; rootEventId?: string }) {
return (
<Note.Provider event={event}>
<Note.Root className="py-2">

View File

@ -1,6 +1,6 @@
import { RepostIcon } from "@lume/icons";
import { Event } from "@lume/types";
import { cn } from "@lume/utils";
import { NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { Note } from "..";
@ -10,7 +10,7 @@ import { User } from "../../user";
export function RepostNote({
event,
className,
}: { event: NDKEvent; className?: string }) {
}: { event: Event; className?: string }) {
const ark = useArk();
const { t } = useTranslation();
@ -23,11 +23,11 @@ export function RepostNote({
queryFn: async () => {
try {
if (event.content.length > 50) {
const embed = JSON.parse(event.content) as NostrEvent;
return new NDKEvent(ark.ndk, embed);
const embed = JSON.parse(event.content) as Event;
return embed;
}
const id = event.tags.find((el) => el[0] === "e")[1];
return await ark.getEventById(id);
return await ark.get_event(id);
} catch {
throw new Error("Failed to get repost event");
}

View File

@ -1,11 +1,11 @@
import { Event } from "@lume/types";
import { cn } from "@lume/utils";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { Note } from "..";
export function TextNote({
event,
className,
}: { event: NDKEvent; className?: string }) {
}: { event: Event; className?: string }) {
return (
<Note.Provider event={event}>
<Note.Root

View File

@ -21,7 +21,7 @@ export function ThreadNote({ eventId }: { eventId: string }) {
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
<User.Time time={data.created_at} />
<span>·</span>
<User.NIP05 pubkey={data.pubkey} />
<User.NIP05 />
</div>
</div>
</User.Root>

View File

@ -1,12 +1,12 @@
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { Event } from "@lume/types";
import { ReactNode, createContext, useContext } from "react";
const EventContext = createContext<NDKEvent>(null);
const EventContext = createContext<Event>(null);
export function NoteProvider({
event,
children,
}: { event: NDKEvent; children: ReactNode }) {
}: { event: Event; children: ReactNode }) {
return (
<EventContext.Provider value={event}>{children}</EventContext.Provider>
);

View File

@ -20,7 +20,6 @@ export function NoteThread({
});
const { t } = useTranslation();
const { addColumn } = useColumnContext();
if (!thread) return null;
@ -42,13 +41,6 @@ export function NoteThread({
</Link>
<button
type="button"
onClick={async () =>
await addColumn({
kind: COL_TYPES.thread,
title: "Thread",
content: thread?.rootEventId || thread?.replyEventId,
})
}
className="inline-flex items-center justify-center rounded-md text-neutral-600 dark:text-neutral-400 size-6 bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
>
<PinIcon className="size-4" />

View File

@ -36,10 +36,7 @@ export function NoteUser({
<div className="flex flex-col gap-2">
<div>
<User.Name className="font-semibold leading-tight" />
<User.NIP05
pubkey={event.pubkey}
className="text-neutral-600 dark:text-neutral-400"
/>
<User.NIP05 className="text-neutral-600 dark:text-neutral-400" />
</div>
<User.About className="line-clamp-3" />
<Link

View File

@ -4,7 +4,7 @@ import { useUserContext } from "./provider";
export function UserAbout({ className }: { className?: string }) {
const user = useUserContext();
if (!user) {
if (!user.profile) {
return (
<div className="flex flex-col gap-1">
<div

View File

@ -18,7 +18,7 @@ export function UserAvatar({ className }: { className?: string }) {
[user],
);
if (!user) {
if (!user.profile) {
return (
<div className="shrink-0">
<div

View File

@ -4,7 +4,7 @@ import { useUserContext } from "./provider";
export function UserName({ className }: { className?: string }) {
const user = useUserContext();
if (!user) {
if (!user.profile) {
return (
<div
className={cn(

View File

@ -16,7 +16,7 @@ export function UserNip05({ className }: { className?: string }) {
enabled: !!user,
});
if (!user) {
if (!user.profile) {
return (
<div
className={cn(

View File

@ -1,4 +1,4 @@
import { PropsWithChildren, createContext, useMemo } from "react";
import { PropsWithChildren, createContext, useEffect, useMemo } from "react";
import { Ark } from "./ark";
export const ArkContext = createContext<Ark>(undefined);

View File

@ -1,5 +1,6 @@
import { RepostNote, TextNote, useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types";
import { EmptyFeed } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
@ -30,18 +31,7 @@ export function HomeRoute({ colKey }: { colKey: string }) {
signal: AbortSignal;
pageParam: number;
}) => {
if (!ark.account.contacts.length) return [];
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: ark.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
const events = await ark.get_text_events(FETCH_LIMIT);
return events;
},
getNextPageParam: (lastPage) => {
@ -54,11 +44,11 @@ export function HomeRoute({ colKey }: { colKey: string }) {
refetchOnMount: false,
});
const renderItem = (event: NDKEvent) => {
const renderItem = (event: Event) => {
switch (event.kind) {
case NDKKind.Text:
case Kind.Text:
return <TextNote key={event.id} event={event} className="mt-3" />;
case NDKKind.Repost:
case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mt-3" />;
default:
return <TextNote key={event.id} event={event} className="mt-3" />;
@ -81,6 +71,7 @@ export function HomeRoute({ colKey }: { colKey: string }) {
};
}, []);
/*
if (!ark.account.contacts.length) {
return (
<div className="px-3 mt-3">
@ -95,6 +86,7 @@ export function HomeRoute({ colKey }: { colKey: string }) {
</div>
);
}
*/
return (
<div className="w-full h-full">

View File

@ -1,17 +1,15 @@
import { Column, useArk } from "@lume/ark";
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { EventRoute, SuggestRoute, UserRoute } from "@lume/ui";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useQueryClient } from "@tanstack/react-query";
import { useRef } from "react";
import { HomeRoute } from "./home";
export function Timeline({ column }: { column: IColumn }) {
const colKey = `timeline-${column.id}`;
const ark = useArk();
const queryClient = useQueryClient();
const since = useRef(Math.floor(Date.now() / 1000));
// const ark = useArk();
// const queryClient = useQueryClient();
// const since = useRef(Math.floor(Date.now() / 1000));
/*
const refresh = async (events: NDKEvent[]) => {
const uniqEvents = new Set(events);
await queryClient.setQueryData(
@ -22,11 +20,12 @@ export function Timeline({ column }: { column: IColumn }) {
}),
);
};
*/
return (
<Column.Root>
<Column.Header id={column.id} queryKey={[colKey]} title="Timeline" />
{ark.account.contacts.length ? (
{/*<Column.Header id={column.id} queryKey={[colKey]} title="Timeline" />*/}
{/*ark.account.contacts.length ? (
<Column.Live
filter={{
kinds: [NDKKind.Text, NDKKind.Repost],
@ -35,15 +34,17 @@ export function Timeline({ column }: { column: IColumn }) {
}}
onClick={refresh}
/>
) : null}
) : null*/}
<Column.Content>
<Column.Route path="/" element={<HomeRoute colKey={colKey} />} />
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
<Column.Route
path="/suggest"
element={<SuggestRoute queryKey={[colKey]} />}
/>
{/*
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
<Column.Route
path="/suggest"
element={<SuggestRoute queryKey={[colKey]} />}
/>
*/}
</Column.Content>
</Column.Root>
);

View File

@ -41,9 +41,10 @@ export interface Metadata {
}
export interface CurrentAccount {
npub: string;
contacts: string[];
interests: Interests;
pubkey: string;
npub?: string;
contacts?: string[];
interests?: Interests;
}
export interface Interests {

View File

@ -29,7 +29,7 @@ export function ActiveAccount() {
<div className="relative">
<Avatar.Root>
<Avatar.Image
src={user?.picture || user?.image}
src={user?.picture}
alt={ark.account.pubkey}
loading="lazy"
decoding="async"

View File

@ -4,6 +4,7 @@ import { useStorage } from "@lume/storage";
import { NDKCacheUserProfile } from "@lume/types";
import { COL_TYPES, cn, editorValueAtom } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { invoke } from "@tauri-apps/api/core";
import { useAtom } from "jotai";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@ -187,8 +188,6 @@ const Element = (props) => {
};
export function EditorForm() {
const ark = useArk();
const storage = useStorage();
const ref = useRef<HTMLDivElement | null>();
const [editorValue, setEditorValue] = useAtom(editorValueAtom);
@ -202,7 +201,6 @@ export function EditorForm() {
);
const { t } = useTranslation();
const { addColumn } = useColumnContext();
const filters = contacts
?.filter((c) => c?.name?.toLowerCase().startsWith(search.toLowerCase()))
@ -242,32 +240,24 @@ export function EditorForm() {
try {
setLoading(true);
const event = new NDKEvent(ark.ndk);
event.kind = NDKKind.Text;
event.content = serialize(editor.children);
const publish = await event.publish();
const content = serialize(editor.children);
const publish = await invoke("publish", { content });
if (publish) {
console.log(publish);
toast.success(t("editor.successMessage"));
// add current post as column thread
addColumn({
kind: COL_TYPES.thread,
content: event.id,
title: "Thread",
});
setLoading(false);
return reset();
}
setLoading(false);
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
/*
useEffect(() => {
async function loadContacts() {
const res = await storage.getAllCacheUsers();
@ -276,6 +266,7 @@ export function EditorForm() {
loadContacts();
}, []);
*/
useEffect(() => {
if (target && filters.length > 0) {

View File

@ -1,4 +1,4 @@
import { Note, User, useArk, useColumnContext } from "@lume/ark";
import { Note, User, useArk } from "@lume/ark";
import { LoaderIcon, SearchIcon } from "@lume/icons";
import { COL_TYPES, searchAtom } from "@lume/utils";
import { type NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
@ -18,7 +18,6 @@ export function SearchDialog() {
const [value] = useDebounce(search, 1200);
const { t } = useTranslation();
const { vlistRef, columns, addColumn } = useColumnContext();
const searchEvents = async () => {
if (!value.length) return;

View File

@ -1,3 +1,2 @@
pub mod folder;
pub mod opg;
pub mod secret;

View File

@ -1,25 +0,0 @@
use keyring::Entry;
#[tauri::command]
pub fn secure_save(key: String, value: String) -> Result<(), ()> {
let entry = Entry::new("Lume", &key).expect("Failed to create entry");
let _ = entry.set_password(&value);
Ok(())
}
#[tauri::command]
pub fn secure_load(key: String) -> Result<String, String> {
let entry = Entry::new("Lume", &key).expect("Failed to create entry");
if let Ok(password) = entry.get_password() {
Ok(password.into())
} else {
Err("Not found".into())
}
}
#[tauri::command]
pub fn secure_remove(key: String) -> Result<(), ()> {
let entry = Entry::new("Lume", &key).expect("Failed to remove entry");
let _ = entry.delete_password();
Ok(())
}

View File

@ -23,6 +23,7 @@ use tauri_plugin_autostart::MacosLauncher;
pub struct Nostr {
pub client: Arc<Client>,
pub client_user: Option<XOnlyPublicKey>,
pub contact_list: Option<Vec<Contact>>,
}
@ -90,17 +91,22 @@ fn main() {
client.connect().await;
// Prepare contact list
let mut user = None;
let mut contact_list = None;
// Run somethings if account existed
if let Some(key) = stored_nsec_key {
let secret_key = SecretKey::from_bech32(key).expect("Get secret key failed");
let keys = Keys::new(secret_key);
let public_key = keys.public_key();
let signer = ClientSigner::Keys(keys);
// Update client's signer
client.set_signer(Some(signer)).await;
// Update user
user = Some(public_key);
// Get contact list
contact_list = Some(
client
@ -113,6 +119,7 @@ fn main() {
// Init global state
handle.manage(Nostr {
client: client.into(),
client_user: user.into(),
contact_list: contact_list.into(),
})
});
@ -152,9 +159,6 @@ fn main() {
nostr::event::repost,
nostr::event::upvote,
nostr::event::downvote,
commands::secret::secure_save,
commands::secret::secure_load,
commands::secret::secure_remove,
commands::folder::show_in_folder,
commands::opg::fetch_opg,
])

View File

@ -8,12 +8,10 @@ pub async fn get_event(id: &str, nostr: State<'_, Nostr>) -> Result<String, ()>
let client = &nostr.client;
let event_id;
if id.starts_with("note1") {
event_id = EventId::from_bech32(id).unwrap();
} else if id.starts_with("nevent1") {
event_id = EventId::from_bech32(id).unwrap();
} else if id.starts_with("naddr1") {
if id.starts_with("note") {
event_id = EventId::from_bech32(id).unwrap();
} else if id.starts_with("nevent") {
event_id = Nip19Event::from_bech32(id).unwrap().event_id;
} else {
event_id = EventId::from_hex(id).unwrap();
}

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<(), ()> {
pub fn save_key(nsec: &str, app_handle: tauri::AppHandle) -> Result<bool, 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();
@ -50,9 +50,9 @@ pub fn save_key(nsec: &str, app_handle: tauri::AppHandle) -> Result<(), ()> {
.expect("Write nsec failed");
writer.finish().expect("Save nsec failed");
Ok(())
Ok(true)
} else {
Err(())
Err(false)
}
}
@ -76,11 +76,19 @@ pub async fn update_signer(nsec: &str, nostr: State<'_, Nostr>) -> Result<(), ()
}
#[tauri::command]
pub async fn verify_signer(nostr: State<'_, Nostr>) -> Result<bool, ()> {
pub async fn verify_signer(nostr: State<'_, Nostr>) -> Result<String, ()> {
let client = &nostr.client;
let status = client.signer().await.is_ok();
let user = &nostr.client_user;
Ok(status)
if let Ok(_) = client.signer().await {
if let Some(public_key) = user {
Ok(public_key.to_string())
} else {
Err(())
}
} else {
Err(())
}
}
#[tauri::command]