feat: add new account management

This commit is contained in:
reya 2024-02-08 17:17:45 +07:00
parent d7bbda6e7b
commit 17052aeeaa
35 changed files with 1140 additions and 1484 deletions

View File

@ -1,5 +1,5 @@
{
"name": "lume",
"name": "@lume/desktop",
"private": true,
"version": "3.0.0",
"scripts": {

View File

@ -1,10 +1,18 @@
import { ColumnProvider, LumeProvider } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
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 Router from "./router";
import { ErrorScreen } from "./routes/error";
const queryClient = new QueryClient({
defaultOptions: {
@ -14,17 +22,278 @@ 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>
<LumeProvider>
<ColumnProvider>
<Router />
</ColumnProvider>
</LumeProvider>
<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 }}
/>
</StorageProvider>
</QueryClientProvider>
</I18nextProvider>

View File

@ -1,285 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { AppLayout, AuthLayout, HomeLayout, SettingsLayout } from "@lume/ui";
import { fetch } from "@tauri-apps/plugin-http";
import {
RouterProvider,
createBrowserRouter,
defer,
redirect,
} from "react-router-dom";
import { ErrorScreen } from "./routes/error";
export default function Router() {
const ark = useArk();
const storage = useStorage();
const router = createBrowserRouter([
{
element: <AppLayout platform={storage.platform} />,
children: [
{
path: "/",
element: <HomeLayout />,
errorElement: <ErrorScreen />,
loader: async () => {
if (!ark.account) return redirect("auth");
return null;
},
children: [
{
index: true,
async lazy() {
const { HomeScreen } = await import("./routes/home");
return { Component: HomeScreen };
},
},
],
},
{
path: "settings",
element: <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,
loader: () => {
const depot = storage.checkDepot();
if (!depot) return redirect("/depot/onboarding/");
return null;
},
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",
element: <AuthLayout platform={storage.platform} />,
errorElement: <ErrorScreen />,
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();
},
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

@ -1,23 +1,19 @@
import { useArk } from "@lume/ark";
import { CheckIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { Keys } from "@lume/types";
import { onboardingAtom } from "@lume/utils";
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import * as Checkbox from "@radix-ui/react-checkbox";
import { invoke } from "@tauri-apps/api/core";
import { desktopDir } from "@tauri-apps/api/path";
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { useSetAtom } from "jotai";
import { nanoid } from "nanoid";
import { getPublicKey, nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
export function CreateAccountKeys() {
const ark = useArk();
const storage = useStorage();
const setOnboarding = useSetAtom(onboardingAtom);
const navigate = useNavigate();
@ -31,36 +27,14 @@ export function CreateAccountKeys() {
try {
setLoading(true);
const privkey = nip19.decode(key).data as string;
const signer = new NDKPrivateKeySigner(privkey);
const pubkey = getPublicKey(privkey);
ark.updateNostrSigner({ signer });
const downloadPath = await desktopDir();
const fileName = `nostr_keys_${nanoid(4)}.txt`;
const filePath = await save({
defaultPath: `${downloadPath}/${fileName}`,
});
if (!filePath) {
return toast.info("You need to save account keys before continue.");
}
await writeTextFile(
filePath,
`Nostr Account\nGenerated by Lume (lume.nu)\n---\nPrivate key: ${key}`,
);
const newAccount = await storage.createAccount({
pubkey: pubkey,
privkey: privkey,
});
ark.account = newAccount;
// trigger save key
await invoke("save_key", { nsec: key });
// update state
setLoading(false);
setOnboarding({ open: true, newUser: true });
// redirect to next step
return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setLoading(false);
@ -69,8 +43,11 @@ export function CreateAccountKeys() {
};
useEffect(() => {
const privkey = NDKPrivateKeySigner.generate().privateKey;
setKey(nip19.nsecEncode(privkey));
async function createAccountKeys() {
const keys: Keys = await invoke("create_keys");
setKey(keys.nsec);
}
createAccountKeys();
}, []);
return (

View File

@ -1,8 +1,5 @@
import { useArk } from "@lume/ark";
import { useStorage } from "@lume/storage";
import { downloadDir } from "@tauri-apps/api/path";
import { message, save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { relaunch } from "@tauri-apps/plugin-process";
import { useRouteError } from "react-router-dom";
@ -12,8 +9,6 @@ interface RouteError {
}
export function ErrorScreen() {
const ark = useArk();
const storage = useStorage();
const error = useRouteError() as RouteError;
const restart = async () => {
@ -27,6 +22,7 @@ export function ErrorScreen() {
const filePath = await save({
defaultPath: `${downloadPath}/${fileName}`,
});
/*
const nsec = await storage.loadPrivkey(ark.account.pubkey);
if (filePath) {
@ -42,6 +38,7 @@ export function ErrorScreen() {
);
}
} // else { user cancel action }
*/
} catch (e) {
await message(e, {
title: "Cannot download account keys",

View File

@ -1,17 +1,10 @@
import react from "@vitejs/plugin-react-swc";
import million from "million/compiler";
import { defineConfig } from "vite";
import topLevelAwait from "vite-plugin-top-level-await";
import viteTsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
million.vite({
auto: {
threshold: 0.05,
},
mute: true,
}),
react(),
viteTsconfigPaths(),
topLevelAwait({

View File

@ -6,6 +6,7 @@
"dependencies": {
"@getalby/sdk": "^3.2.3",
"@lume/icons": "workspace:^",
"@lume/storage": "workspace:^",
"@lume/utils": "workspace:^",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",

View File

@ -4,8 +4,8 @@ import { invoke } from "@tauri-apps/api/core";
export class Ark {
public account: CurrentAccount;
constructor(account: CurrentAccount) {
this.account = account;
constructor() {
this.account = null;
}
public async event_to_bech32(id: string, relays: string[]) {
@ -67,12 +67,12 @@ export class Ark {
};
}
public async get_metadata(id: string) {
public async get_profile(id: string) {
try {
const cmd: Metadata = await invoke("get_metadata", { id });
const cmd: Metadata = await invoke("get_profile", { id });
return cmd;
} catch (e) {
console.error("failed to get metadata", id);
console.error("failed to get profile", id);
}
}

View File

@ -3,7 +3,6 @@ import { cn, editorAtom, editorValueAtom } from "@lume/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useSetAtom } from "jotai";
import { nip19 } from "nostr-tools";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";

View File

@ -31,7 +31,7 @@ export function UserAbout({ className }: { className?: string }) {
return (
<div className={cn("select-text break-p", className)}>
{user.about?.trim() || user.bio?.trim() || "No bio"}
{user.profile.about?.trim() || "No bio"}
</div>
);
}

View File

@ -46,7 +46,7 @@ export function UserAvatar({ className }: { className?: string }) {
/>
) : (
<Avatar.Image
src={user.image}
src={user.profile.picture}
alt={user.pubkey}
loading="eager"
decoding="async"

View File

@ -15,7 +15,7 @@ export function UserCover({ className }: { className?: string }) {
);
}
if (user && !user.banner) {
if (user && !user.profile.banner) {
return (
<div
className={cn("bg-gradient-to-b from-sky-400 to-sky-200", className)}
@ -25,7 +25,7 @@ export function UserCover({ className }: { className?: string }) {
return (
<img
src={user.banner}
src={user.profile.banner}
alt="banner"
loading="lazy"
decoding="async"

View File

@ -17,7 +17,7 @@ export function UserName({ className }: { className?: string }) {
return (
<div className={cn("max-w-[12rem] truncate", className)}>
{user.displayName || user.name || "Anon"}
{user.profile.display_name || user.profile.name || "Anon"}
</div>
);
}

View File

@ -1,26 +1,17 @@
import { VerifiedIcon } from "@lume/icons";
import { cn, displayNpub } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { useArk } from "../../hooks/useArk";
import { useUserContext } from "./provider";
export function UserNip05({
pubkey,
className,
}: { pubkey: string; className?: string }) {
const ark = useArk();
export function UserNip05({ className }: { className?: string }) {
const user = useUserContext();
const { isLoading, data: verified } = useQuery({
queryKey: ["nip05", user?.nip05],
queryKey: ["nip05", user?.profile.nip05],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
if (!user) return false;
if (!user.nip05) return false;
return ark.validateNIP05({
pubkey,
nip05: user.nip05,
signal,
});
if (!user.profile.nip05) return false;
return false;
},
enabled: !!user,
});
@ -39,11 +30,11 @@ export function UserNip05({
return (
<div className="inline-flex items-center gap-1">
<p className={cn("text-sm", className)}>
{!user?.nip05
? displayNpub(pubkey, 16)
: user?.nip05?.startsWith("_@")
? user?.nip05?.replace("_@", "")
: user?.nip05}
{!user?.profile.nip05
? displayNpub(user.pubkey, 16)
: user?.profile.nip05?.startsWith("_@")
? user?.profile.nip05?.replace("_@", "")
: user?.profile.nip05}
</p>
{!isLoading && verified ? (
<VerifiedIcon className="size-4 text-teal-500" />

View File

@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query";
import { ReactNode, createContext, useContext } from "react";
import { useArk } from "../../hooks/useArk";
const UserContext = createContext<Metadata>(null);
const UserContext = createContext<{ pubkey: string; profile: Metadata }>(null);
export function UserProvider({
pubkey,
@ -11,12 +11,12 @@ export function UserProvider({
embed,
}: { pubkey: string; children: ReactNode; embed?: string }) {
const ark = useArk();
const { data: user } = useQuery({
const { data: profile } = useQuery({
queryKey: ["user", pubkey],
queryFn: async () => {
if (embed) return JSON.parse(embed) as Metadata;
const profile = await ark.get_metadata(pubkey);
const profile = await ark.get_profile(pubkey);
if (!profile)
throw new Error(
@ -32,7 +32,11 @@ export function UserProvider({
retry: 2,
});
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
return (
<UserContext.Provider value={{ pubkey, profile }}>
{children}
</UserContext.Provider>
);
}
export function useUserContext() {

View File

@ -1,4 +0,0 @@
import { createContext } from "react";
import { type Ark } from "./ark";
export const LumeContext = createContext<Ark>(undefined);

View File

@ -1,8 +1,8 @@
import { useContext } from "react";
import { LumeContext } from "../context";
import { ArkContext } from "../provider";
export const useArk = () => {
const context = useContext(LumeContext);
const context = useContext(ArkContext);
if (context === undefined) {
throw new Error("Please import Ark Provider to use useArk() hook");
}

View File

@ -1,4 +1,4 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { useArk } from "./useArk";
export function useProfile(pubkey: string) {
@ -10,7 +10,7 @@ export function useProfile(pubkey: string) {
} = useQuery({
queryKey: ["user", pubkey],
queryFn: async () => {
const profile = await ark.get_metadata(pubkey);
const profile = await ark.get_profile(pubkey);
if (!profile)
throw new Error(
`Cannot get metadata for ${pubkey}, will be retry after 10 seconds`,

View File

@ -1,6 +1,4 @@
import { NDKKind, NDKTag } from "@nostr-dev-kit/ndk";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { normalizeRelayUrl } from "nostr-fetch";
import { useArk } from "./useArk";
export function useRelaylist() {

View File

@ -1,5 +1,4 @@
export * from "./ark";
export * from "./context";
export * from "./provider";
export * from "./hooks/useEvent";
export * from "./hooks/useArk";

View File

@ -1,18 +1,9 @@
import { PropsWithChildren, useEffect, useState } from "react";
import { PropsWithChildren, createContext, useMemo } from "react";
import { Ark } from "./ark";
import { LumeContext } from "./context";
export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
const [ark, setArk] = useState<Ark>(undefined);
export const ArkContext = createContext<Ark>(undefined);
useEffect(() => {
async function setupArk() {
const _ark = new Ark();
setArk(_ark);
}
if (!ark) setupArk();
}, []);
return <LumeContext.Provider value={ark}>{children}</LumeContext.Provider>;
export const ArkProvider = ({ children }: PropsWithChildren<object>) => {
const ark = useMemo(() => new Ark(), []);
return <ArkContext.Provider value={ark}>{children}</ArkContext.Provider>;
};

View File

@ -8,7 +8,7 @@
"access": "public"
},
"dependencies": {
"nostr-tools": "~1.17.0",
"@tauri-apps/plugin-store": "2.0.0-beta.0",
"react": "^18.2.0"
},
"devDependencies": {

View File

@ -1,18 +1,15 @@
import { locale, platform } from "@tauri-apps/plugin-os";
import Database from "@tauri-apps/plugin-sql";
import { Store } from "@tauri-apps/plugin-store";
import { PropsWithChildren, createContext, useContext } from "react";
import { LumeStorage } from "./storage";
const StorageContext = createContext<LumeStorage>(null);
const sqliteAdapter = await Database.load("sqlite:lume_v3.db");
const store = new Store("lume.data");
const platformName = await platform();
const osLocale = await locale();
const db = new LumeStorage(sqliteAdapter, platformName, osLocale);
await db.init();
if (db.settings.depot) await db.launchDepot();
const db = new LumeStorage(store, platformName, osLocale);
export const StorageProvider = ({ children }: PropsWithChildren<object>) => {
return (

View File

@ -1,458 +1,41 @@
import {
Account,
IColumn,
Interests,
NDKCacheEvent,
NDKCacheEventTag,
NDKCacheUser,
NDKCacheUserProfile,
} from "@lume/types";
import { invoke } from "@tauri-apps/api/core";
import { resolve, appConfigDir, resolveResource } from "@tauri-apps/api/path";
import { VITE_FLATPAK_RESOURCE } from "@lume/utils";
import { Platform } from "@tauri-apps/plugin-os";
import { Child, Command } from "@tauri-apps/plugin-shell";
import Database from "@tauri-apps/plugin-sql";
import { nip19 } from "nostr-tools";
import { Store } from "@tauri-apps/plugin-store";
export class LumeStorage {
#db: Database;
#depot: Child;
#store: Store;
readonly platform: Platform;
readonly locale: string;
public currentUser: Account;
public interests: Interests;
public nwc: string;
public settings: {
autoupdate: boolean;
nsecbunker: boolean;
media: boolean;
hashtag: boolean;
depot: boolean;
tunnelUrl: string;
lowPower: boolean;
translation: boolean;
translateApiKey: string;
instantZap: boolean;
defaultZapAmount: number;
};
constructor(db: Database, platform: Platform, locale: string) {
this.#db = db;
constructor(store: Store, platform: Platform, locale: string) {
this.#store = store;
this.locale = locale;
this.platform = platform;
this.interests = null;
this.nwc = null;
this.settings = {
autoupdate: false,
nsecbunker: false,
media: true,
hashtag: true,
depot: false,
tunnelUrl: "",
lowPower: false,
translation: false,
translateApiKey: "",
instantZap: false,
defaultZapAmount: 21,
};
}
public async init() {
const settings = await this.getAllSettings();
const account = await this.getActiveAccount();
if (account) {
this.currentUser = account;
this.interests = await this.getInterests();
}
for (const item of settings) {
if (item.value.length > 10) {
this.settings[item.key] = item.value;
} else {
this.settings[item.key] = !!parseInt(item.value);
}
}
}
async #keyring_save(key: string, value: string) {
return await invoke("secure_save", { key, value });
}
async #keyring_load(key: string) {
try {
const value: string = await invoke("secure_load", { key });
if (!value) return null;
return value;
} catch {
return null;
}
}
async #keyring_remove(key: string) {
return await invoke("secure_remove", { key });
}
public async launchDepot() {
const configPath =
VITE_FLATPAK_RESOURCE !== null
? await resolve("/", VITE_FLATPAK_RESOURCE)
: await resolveResource("resources/config.toml");
const dataPath = await appConfigDir();
const command = Command.sidecar("bin/depot", [
"-c",
configPath,
"-d",
dataPath,
]);
this.#depot = await command.spawn();
}
public checkDepot() {
if (this.#depot) return true;
return false;
}
public async stopDepot() {
if (this.#depot) return this.#depot.kill();
}
public async getCacheUser(pubkey: string) {
const results: Array<NDKCacheUser> = await this.#db.select(
"SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;",
[pubkey],
);
if (!results.length) return null;
if (typeof results[0].profile === "string")
results[0].profile = JSON.parse(results[0].profile);
return results[0];
}
public async getCacheEvent(id: string) {
const results: Array<NDKCacheEvent> = await this.#db.select(
"SELECT * FROM ndk_events WHERE id = $1 ORDER BY id DESC LIMIT 1;",
[id],
);
if (!results.length) return null;
return results[0];
}
public async getCacheEvents(ids: string[]) {
const idsArr = `'${ids.join("','")}'`;
const results: Array<NDKCacheEvent> = await this.#db.select(
`SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;`,
);
if (!results.length) return [];
return results;
}
public async getCacheEventsByPubkey(pubkey: string) {
const results: Array<NDKCacheEvent> = await this.#db.select(
"SELECT * FROM ndk_events WHERE pubkey = $1 ORDER BY id;",
[pubkey],
);
if (!results.length) return [];
return results;
}
public async getCacheEventsByKind(kind: number) {
const results: Array<NDKCacheEvent> = await this.#db.select(
"SELECT * FROM ndk_events WHERE kind = $1 ORDER BY id;",
[kind],
);
if (!results.length) return [];
return results;
}
public async getCacheEventsByKindAndAuthor(kind: number, pubkey: string) {
const results: Array<NDKCacheEvent> = await this.#db.select(
"SELECT * FROM ndk_events WHERE kind = $1 AND pubkey = $2 ORDER BY id;",
[kind, pubkey],
);
if (!results.length) return [];
return results;
}
public async getCacheEventTagsByTagValue(tagValue: string) {
const results: Array<NDKCacheEventTag> = await this.#db.select(
"SELECT * FROM ndk_eventtags WHERE tagValue = $1 ORDER BY id;",
[tagValue],
);
if (!results.length) return [];
return results;
}
public async setCacheEvent({
id,
pubkey,
content,
kind,
createdAt,
relay,
event,
}: NDKCacheEvent) {
return await this.#db.execute(
"INSERT OR IGNORE INTO ndk_events (id, pubkey, content, kind, createdAt, relay, event) VALUES ($1, $2, $3, $4, $5, $6, $7);",
[id, pubkey, content, kind, createdAt, relay, event],
);
}
public async setCacheEventTag({
id,
eventId,
tag,
value,
tagValue,
}: NDKCacheEventTag) {
return await this.#db.execute(
"INSERT OR IGNORE INTO ndk_eventtags (id, eventId, tag, value, tagValue) VALUES ($1, $2, $3, $4, $5);",
[id, eventId, tag, value, tagValue],
);
}
public async setCacheProfiles(profiles: Array<NDKCacheUser>) {
return await Promise.all(
profiles.map(
async (profile) =>
await this.#db.execute(
"INSERT OR IGNORE INTO ndk_users (pubkey, profile, createdAt) VALUES ($1, $2, $3);",
[profile.pubkey, profile.profile, profile.createdAt],
),
),
);
}
public async getAllCacheUsers() {
const results: Array<NDKCacheUser> = await this.#db.select(
"SELECT * FROM ndk_users ORDER BY createdAt DESC;",
);
if (!results.length) return [];
const users: NDKCacheUserProfile[] = results.map((item) => ({
npub: nip19.npubEncode(item.pubkey),
...JSON.parse(item.profile as string),
}));
return users;
}
public async checkAccount() {
const result: Array<{ total: string }> = await this.#db.select(
'SELECT COUNT(*) AS "total" FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;',
);
return parseInt(result[0].total);
}
public async getActiveAccount() {
const results: Array<Account> = await this.#db.select(
'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;',
);
if (results.length) {
this.currentUser = results[0];
return results[0];
}
return null;
}
public async createAccount({
pubkey,
privkey,
}: {
pubkey: string;
privkey?: string;
}) {
const existAccounts: Array<Account> = await this.#db.select(
"SELECT * FROM accounts WHERE pubkey = $1 ORDER BY id DESC LIMIT 1;",
[pubkey],
);
if (existAccounts.length) {
await this.#db.execute(
"UPDATE accounts SET is_active = '1' WHERE pubkey = $1;",
[pubkey],
);
} else {
await this.#db.execute(
"INSERT OR IGNORE INTO accounts (pubkey, is_active) VALUES ($1, $2);",
[pubkey, 1],
);
if (privkey) await this.#keyring_save(pubkey, privkey);
}
const account = await this.getActiveAccount();
return account;
}
/**
* Save private key to OS secure storage
* @deprecated this method will be remove in the next update
*/
public async createPrivkey(name: string, privkey: string) {
return await this.#keyring_save(name, privkey);
}
/**
* Load private key from OS secure storage
* @deprecated this method will be remove in the next update
*/
public async loadPrivkey(name: string) {
return await this.#keyring_load(name);
}
/**
* Remove private key from OS secure storage
* @deprecated this method will be remove in the next update
*/
public async removePrivkey(name: string) {
return await this.#keyring_remove(name);
}
public async updateAccount(column: string, value: string) {
const insert = await this.#db.execute(
`UPDATE accounts SET ${column} = $1 WHERE id = $2;`,
[value, this.currentUser.id],
);
if (insert) {
const account = await this.getActiveAccount();
return account;
}
}
public async getColumns() {
if (!this.currentUser) return [];
const columns: Array<IColumn> = await this.#db.select(
"SELECT * FROM columns WHERE account_id = $1 ORDER BY created_at DESC;",
[this.currentUser.id],
);
return columns;
}
public async createColumn(
kind: number,
title: string,
content: string | string[],
) {
const insert = await this.#db.execute(
"INSERT INTO columns (account_id, kind, title, content) VALUES ($1, $2, $3, $4);",
[this.currentUser.id, kind, title, content],
);
if (insert) {
const columns: Array<IColumn> = await this.#db.select(
"SELECT * FROM columns WHERE id = $1 ORDER BY id DESC LIMIT 1;",
[insert.lastInsertId],
);
if (!columns.length) console.error("get created widget failed");
return columns[0];
}
}
public async updateColumn(id: number, title: string, content: string) {
return await this.#db.execute(
"UPDATE columns SET title = $1, content = $2 WHERE id = $3;",
[title, content, id],
);
}
public async removeColumn(id: number) {
const res = await this.#db.execute("DELETE FROM columns WHERE id = $1;", [
id,
]);
if (res) return id;
}
public async createSetting(key: string, value: string | undefined) {
const currentSetting = await this.checkSettingValue(key);
if (!currentSetting) {
if (key !== "translateApiKey" && key !== "tunnelUrl")
this.settings[key] === !!parseInt(value);
return await this.#db.execute(
"INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);",
[key, value],
);
}
return await this.#db.execute(
"UPDATE settings SET value = $1 WHERE key = $2;",
[value, key],
);
}
public async getAllSettings() {
const results: { key: string; value: string }[] = await this.#db.select(
"SELECT * FROM settings ORDER BY id DESC;",
);
if (results.length < 1) return [];
return results;
}
public async checkSettingValue(key: string) {
const results: { key: string; value: string }[] = await this.#db.select(
"SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;",
[key],
);
if (!results.length) return false;
return results[0].value;
}
public async getSettingValue(key: string) {
const results: { key: string; value: string }[] = await this.#db.select(
"SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;",
[key],
);
if (!results.length) return "0";
return results[0].value;
}
public async getInterests() {
const results: { key: string; value: string }[] = await this.#db.select(
"SELECT * FROM settings WHERE key = 'interests' ORDER BY id DESC LIMIT 1;",
);
if (!results.length) return null;
if (!results[0].value.length) return null;
return JSON.parse(results[0].value) as Interests;
}
public async clearCache() {
await this.#db.execute("DELETE FROM ndk_events;");
await this.#db.execute("DELETE FROM ndk_eventtags;");
await this.#db.execute("DELETE FROM ndk_users;");
}
public async clearProfileCache(pubkey: string) {
await this.#db.execute("DELETE FROM ndk_users WHERE pubkey = $1;", [
pubkey,
]);
}
public async logout() {
await this.createSetting("nsecbunker", "0");
await this.#db.execute(
"UPDATE accounts SET is_active = '0' WHERE id = $1;",
[this.currentUser.id],
);
this.currentUser = null;
this.nwc = null;
public async createSetting(key: string, value: string | boolean) {
this.settings[key] = value;
await this.#store.set(this.settings[key], { value });
}
}

View File

@ -1,6 +1,7 @@
{
"name": "@lume/types",
"version": "0.0.0",
"main": "./index.d.ts",
"types": "./index.d.ts",
"private": true,
"license": "MIT",

View File

@ -1,24 +1,21 @@
import { useStorage } from "@lume/storage";
import { cn } from "@lume/utils";
import { type Platform } from "@tauri-apps/plugin-os";
import { Outlet } from "react-router-dom";
import { Editor } from "../editor/column";
import { Navigation } from "../navigation";
import { SearchDialog } from "../search/dialog";
import { WindowTitleBar } from "../titlebar";
export function AppLayout({ platform }: { platform: Platform }) {
export function AppLayout() {
const storage = useStorage();
return (
<div
className={cn(
"flex h-screen w-screen flex-col",
platform !== "macos" ? "bg-neutral-50 dark:bg-neutral-950" : "",
storage.platform !== "macos" ? "bg-neutral-50 dark:bg-neutral-950" : "",
)}
>
{platform === "windows" ? (
<WindowTitleBar platform={platform} />
) : (
<div data-tauri-drag-region className="h-9 shrink-0" />
)}
<div data-tauri-drag-region className="h-9 shrink-0" />
<div className="flex w-full h-full min-h-0">
<Navigation />
<Editor />

View File

@ -1,9 +1,7 @@
import { ArrowLeftIcon, SettingsIcon } from "@lume/icons";
import { type Platform } from "@tauri-apps/plugin-os";
import { ArrowLeftIcon } from "@lume/icons";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { WindowTitleBar } from "../titlebar";
export function AuthLayout({ platform }: { platform: Platform }) {
export function AuthLayout() {
const location = useLocation();
const navigate = useNavigate();
@ -11,11 +9,7 @@ export function AuthLayout({ platform }: { platform: Platform }) {
return (
<div className="flex flex-col w-screen h-screen bg-black text-neutral-50">
{platform === "windows" ? (
<WindowTitleBar platform={platform} />
) : (
<div data-tauri-drag-region className="h-9 shrink-0" />
)}
<div data-tauri-drag-region className="h-9 shrink-0" />
<div className="relative w-full h-full">
<div className="absolute top-8 z-10 flex items-center justify-between w-full px-9">
{canGoBack ? (

View File

@ -345,30 +345,33 @@ importers:
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/storage':
specifier: workspace:^
version: link:../storage
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@radix-ui/react-avatar':
specifier: ^1.0.4
version: 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-collapsible':
specifier: ^1.0.3
version: 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
version: 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dialog':
specifier: ^1.0.5
version: 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.6
version: 2.0.6(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
version: 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-hover-card':
specifier: ^1.0.7
version: 1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
version: 1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-popover':
specifier: ^1.0.7
version: 1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
version: 1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.0.7
version: 1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
version: 1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query':
specifier: ^5.18.1
version: 5.18.1(react@18.2.0)
@ -1052,9 +1055,9 @@ importers:
packages/storage:
dependencies:
nostr-tools:
specifier: ~1.17.0
version: 1.17.0(typescript@5.3.3)
'@tauri-apps/plugin-store':
specifier: 2.0.0-beta.0
version: 2.0.0-beta.0
react:
specifier: ^18.2.0
version: 18.2.0
@ -2569,26 +2572,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-arrow@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-avatar@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==}
peerDependencies:
@ -2613,29 +2596,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-avatar@1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==}
peerDependencies:
@ -2692,33 +2652,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collapsible@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
peerDependencies:
@ -2743,29 +2676,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collection@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.52)(react@18.2.0):
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
@ -2828,39 +2738,6 @@ packages:
react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0)
dev: false
/@radix-ui/react-dialog@1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0)
dev: false
/@radix-ui/react-direction@1.0.1(@types/react@18.2.52)(react@18.2.0):
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
peerDependencies:
@ -2900,30 +2777,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-dismissable-layer@1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==}
peerDependencies:
@ -2951,32 +2804,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-dropdown-menu@2.0.6(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-menu': 2.0.6(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.52)(react@18.2.0):
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
peerDependencies:
@ -3014,28 +2841,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-focus-scope@1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-hover-card@1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==}
peerDependencies:
@ -3065,34 +2870,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-hover-card@1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-popper': 1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-id@1.0.1(@types/react@18.2.52)(react@18.2.0):
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
peerDependencies:
@ -3146,43 +2923,6 @@ packages:
react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0)
dev: false
/@radix-ui/react-menu@2.0.6(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-popper': 1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-roving-focus': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0)
dev: false
/@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==}
peerDependencies:
@ -3218,40 +2958,6 @@ packages:
react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0)
dev: false
/@radix-ui/react-popover@1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-popper': 1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0)
dev: false
/@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==}
peerDependencies:
@ -3282,35 +2988,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-popper@1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-arrow': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/rect': 1.0.1
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
peerDependencies:
@ -3332,26 +3009,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-portal@1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
peerDependencies:
@ -3374,27 +3031,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-presence@1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
peerDependencies:
@ -3416,26 +3052,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-primitive@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
peerDependencies:
@ -3465,34 +3081,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-roving-focus@1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-select@2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==}
peerDependencies:
@ -3608,37 +3196,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-tooltip@1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-popper': 1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0)
'@radix-ui/react-visually-hidden': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.52)(react@18.2.0):
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
@ -3762,26 +3319,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-visually-hidden@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.52
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/rect@1.0.1:
resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==}
dependencies:
@ -4424,6 +3961,12 @@ packages:
'@tauri-apps/api': 2.0.0-beta.0
dev: false
/@tauri-apps/plugin-store@2.0.0-beta.0:
resolution: {integrity: sha512-DT3pzMyNcgO90hDgmnN7j5fYQIaaD54gbi0oKi7n4Nwa6y5GqHsgpnzot9IBSOTS6kYy6D8yrN43XN/xwG4vUg==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.0
dev: false
/@tauri-apps/plugin-updater@2.0.0-beta.0:
resolution: {integrity: sha512-TkKzngrgg8dQOr869OcObLdN10yXNiT/ERQp7sRYvV0vMpRJhYSIwTkpF+UkZGuEXtSqqE0FJEnb+4WuCelMdw==}
dependencies:

521
src-tauri/Cargo.lock generated
View File

@ -17,6 +17,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.7.5"
@ -40,6 +50,49 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "age"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edeef7d7b199195a2d7d7a8155d2d04aee736e60c5c7bdd7097d115369a8817d"
dependencies = [
"age-core",
"base64",
"bech32",
"chacha20poly1305",
"cookie-factory",
"hmac",
"i18n-embed",
"i18n-embed-fl",
"lazy_static",
"nom",
"pin-project",
"rand 0.8.5",
"rust-embed",
"scrypt",
"sha2",
"subtle",
"x25519-dalek",
"zeroize",
]
[[package]]
name = "age-core"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5f11899bc2bbddd135edbc30c36b1924fa59d0746bb45beb5933fafe3fe509b"
dependencies = [
"base64",
"chacha20poly1305",
"cookie-factory",
"hkdf",
"io_tee",
"nom",
"rand 0.8.5",
"secrecy",
"sha2",
]
[[package]]
name = "ahash"
version = "0.8.7"
@ -170,6 +223,12 @@ dependencies = [
"x11rb 0.12.0",
]
[[package]]
name = "arc-swap"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
[[package]]
name = "as-raw-xcb-connection"
version = "1.0.1"
@ -819,6 +878,19 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher 0.4.4",
"poly1305",
"zeroize",
]
[[package]]
name = "chrono"
version = "0.4.33"
@ -849,6 +921,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
]
[[package]]
@ -962,6 +1035,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cookie-factory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -1125,6 +1204,33 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"fiat-crypto",
"platforms",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "darling"
version = "0.20.5"
@ -1160,6 +1266,19 @@ dependencies = [
"syn 2.0.48",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.3",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "data-encoding"
version = "2.5.0"
@ -1306,6 +1425,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "displaydoc"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "dlib"
version = "0.5.2"
@ -1543,6 +1673,12 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "fiat-crypto"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382"
[[package]]
name = "field-offset"
version = "0.3.6"
@ -1565,6 +1701,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "find-crate"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
dependencies = [
"toml 0.5.11",
]
[[package]]
name = "flatbuffers"
version = "23.5.26"
@ -1585,6 +1730,50 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fluent"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7"
dependencies = [
"fluent-bundle",
"unic-langid",
]
[[package]]
name = "fluent-bundle"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd"
dependencies = [
"fluent-langneg",
"fluent-syntax",
"intl-memoizer",
"intl_pluralrules",
"rustc-hash",
"self_cell 0.10.3",
"smallvec",
"unic-langid",
]
[[package]]
name = "fluent-langneg"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94"
dependencies = [
"unic-langid",
]
[[package]]
name = "fluent-syntax"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78"
dependencies = [
"thiserror",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -2372,6 +2561,75 @@ dependencies = [
"tokio-native-tls",
]
[[package]]
name = "i18n-config"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ce3c48cbc21fd5b22b9331f32b5b51f6ad85d969b99e793427332e76e7640"
dependencies = [
"log",
"serde",
"serde_derive",
"thiserror",
"toml 0.8.2",
"unic-langid",
]
[[package]]
name = "i18n-embed"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94205d95764f5bb9db9ea98fa77f89653365ca748e27161f5bbea2ffd50e459c"
dependencies = [
"arc-swap",
"fluent",
"fluent-langneg",
"fluent-syntax",
"i18n-embed-impl",
"intl-memoizer",
"lazy_static",
"log",
"parking_lot",
"rust-embed",
"thiserror",
"unic-langid",
"walkdir 2.4.0",
]
[[package]]
name = "i18n-embed-fl"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc1f8715195dffc4caddcf1cf3128da15fe5d8a137606ea8856c9300047d5a2"
dependencies = [
"dashmap",
"find-crate",
"fluent",
"fluent-syntax",
"i18n-config",
"i18n-embed",
"lazy_static",
"proc-macro-error",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.48",
"unic-langid",
]
[[package]]
name = "i18n-embed-impl"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81093c4701672f59416582fe3145676126fd23ba5db910acad0793c1108aaa58"
dependencies = [
"find-crate",
"i18n-config",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
@ -2488,6 +2746,25 @@ dependencies = [
"web-sys",
]
[[package]]
name = "intl-memoizer"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f"
dependencies = [
"type-map",
"unic-langid",
]
[[package]]
name = "intl_pluralrules"
version = "7.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
dependencies = [
"unic-langid",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
@ -2499,6 +2776,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "io_tee"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304"
[[package]]
name = "ipnet"
version = "2.9.0"
@ -2827,6 +3110,7 @@ dependencies = [
name = "lume"
version = "3.0.0"
dependencies = [
"age",
"keyring",
"nostr-sdk",
"serde",
@ -2960,6 +3244,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minisign-verify"
version = "0.2.1"
@ -3082,6 +3372,16 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nostr"
version = "0.27.0"
@ -3537,6 +3837,16 @@ dependencies = [
"sha2",
]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -3687,6 +3997,26 @@ dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
@ -3716,6 +4046,12 @@ version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb"
[[package]]
name = "platforms"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c"
[[package]]
name = "plist"
version = "1.6.0"
@ -3773,6 +4109,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -4149,12 +4496,52 @@ dependencies = [
"smallvec",
]
[[package]]
name = "rust-embed"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82c0bbc10308ed323529fd3c1dce8badda635aa319a5ff0e6466f33b8101e3f"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir 2.4.0",
]
[[package]]
name = "rust-embed-impl"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.48",
"walkdir 2.4.0",
]
[[package]]
name = "rust-embed-utils"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665"
dependencies = [
"sha2",
"walkdir 2.4.0",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -4240,6 +4627,15 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher 0.4.4",
]
[[package]]
name = "same-file"
version = "0.1.3"
@ -4306,6 +4702,17 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"pbkdf2 0.12.2",
"salsa20",
"sha2",
]
[[package]]
name = "sct"
version = "0.7.1"
@ -4337,6 +4744,15 @@ dependencies = [
"cc",
]
[[package]]
name = "secrecy"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
dependencies = [
"zeroize",
]
[[package]]
name = "secret-service"
version = "3.0.1"
@ -4399,6 +4815,21 @@ dependencies = [
"thin-slice",
]
[[package]]
name = "self_cell"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d"
dependencies = [
"self_cell 1.0.3",
]
[[package]]
name = "self_cell"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
[[package]]
name = "semver"
version = "1.0.21"
@ -5537,6 +5968,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "tinystr"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece"
dependencies = [
"displaydoc",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@ -5643,6 +6083,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.7.8"
@ -5824,6 +6273,15 @@ dependencies = [
"utf-8",
]
[[package]]
name = "type-map"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46"
dependencies = [
"rustc-hash",
]
[[package]]
name = "typenum"
version = "1.17.0"
@ -5841,6 +6299,25 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "unic-langid"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238722e6d794ed130f91f4ea33e01fcff4f188d92337a21297892521c72df516"
dependencies = [
"unic-langid-impl",
]
[[package]]
name = "unic-langid-impl"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd55a2063fdea4ef1f8633243a7b0524cbeef1905ae04c31a1c9b9775c55bc6"
dependencies = [
"serde",
"tinystr",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"
@ -5868,6 +6345,16 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"
@ -6762,6 +7249,18 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34"
[[package]]
name = "x25519-dalek"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [
"curve25519-dalek",
"rand_core 0.6.4",
"serde",
"zeroize",
]
[[package]]
name = "xattr"
version = "1.3.1"
@ -6880,6 +7379,26 @@ dependencies = [
"syn 2.0.48",
]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "zip"
version = "0.6.6"
@ -6894,7 +7413,7 @@ dependencies = [
"crossbeam-utils",
"flate2",
"hmac",
"pbkdf2",
"pbkdf2 0.11.0",
"sha1",
"time",
"zstd",

View File

@ -38,6 +38,7 @@ tauri-plugin-window-state = "2.0.0-beta"
tauri-plugin-theme = { git = "https://github.com/wyhaya/tauri-plugin-theme" }
webpage = { version = "2.0", features = ["serde"] }
keyring = "2"
age = "0.10.0"
[features]
# by default Tauri runs in production mode

View File

@ -1,41 +1,42 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": ["linux", "macOS", "windows"],
"windows": ["main", "settings", "event-*", "user-*", "column-*"],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"theme:allow-set-theme",
"theme:allow-get-theme",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:default",
"os:allow-locale",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**/"
},
{
"url": "https://**/"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$RESOURCE/locales/*"
}
]
}
]
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": ["linux", "macOS", "windows"],
"windows": ["main", "settings", "event-*", "user-*", "column-*"],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"theme:allow-set-theme",
"theme:allow-get-theme",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:default",
"os:allow-locale",
"os:allow-platform",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**/"
},
{
"url": "https://**/"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$RESOURCE/locales/*"
}
]
}
]
}

View File

@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}

View File

@ -6,8 +6,16 @@
pub mod commands;
pub mod nostr;
use age::secrecy::ExposeSecret;
use keyring::Entry;
use nostr_sdk::prelude::*;
use std::error::Error;
use std::fs::File;
use std::io::{BufReader, Read};
use std::iter;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use tauri::Manager;
@ -25,6 +33,39 @@ fn main() {
let handle = app.handle().clone();
let config_dir = handle.path().app_config_dir().unwrap();
let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap();
let mut stored_nsec_key = None;
if let Ok(key) = keyring_entry.get_password() {
let app_key = age::x25519::Identity::from_str(&key.to_string()).unwrap();
if let Ok(nsec_paths) = get_nsec_paths(config_dir.as_path()) {
let last_nsec_path = nsec_paths.last();
if let Some(nsec_path) = last_nsec_path {
let file = File::open(nsec_path).expect("Open nsec file failed");
let file_buf = BufReader::new(file);
let decryptor = match age::Decryptor::new_buffered(file_buf).expect("Decryptor failed")
{
age::Decryptor::Recipients(d) => d,
_ => unreachable!(),
};
let mut decrypted = vec![];
let mut reader = decryptor
.decrypt(iter::once(&app_key as &dyn age::Identity))
.expect("Decrypt nsec file failed");
reader
.read_to_end(&mut decrypted)
.expect("Read secret key failed");
stored_nsec_key = Some(String::from_utf8(decrypted).expect("Not valid"))
}
}
} else {
let app_key = age::x25519::Identity::generate().to_string();
let app_secret = app_key.expose_secret();
let _ = keyring_entry.set_password(app_secret);
}
tauri::async_runtime::spawn(async move {
// Create nostr database connection
let nostr_db = SQLiteDatabase::open(config_dir.join("nostr.db"))
@ -48,13 +89,12 @@ fn main() {
// Connect
client.connect().await;
// Get stored account
let entry = Entry::new("Lume", "Account").unwrap();
// Prepare contact list
let mut contact_list = None;
// Run somethings if account existed
if let Ok(key) = entry.get_password() {
let secret_key = SecretKey::from_bech32(key).unwrap();
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 signer = ClientSigner::Keys(keys);
@ -97,6 +137,7 @@ fn main() {
))
.invoke_handler(tauri::generate_handler![
nostr::keys::create_keys,
nostr::keys::save_key,
nostr::keys::get_public_key,
nostr::keys::update_signer,
nostr::keys::verify_signer,
@ -120,3 +161,19 @@ fn main() {
.run(ctx)
.expect("error while running tauri application");
}
fn get_nsec_paths(dir: &Path) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let paths = std::fs::read_dir(dir)?
.filter_map(|res| res.ok())
.map(|dir_entry| dir_entry.path())
.filter_map(|path| {
if path.extension().map_or(false, |ext| ext == "nsec") {
Some(path)
} else {
None
}
})
.collect::<Vec<_>>();
Ok(paths)
}

View File

@ -1,7 +1,8 @@
use crate::Nostr;
use keyring::Entry;
use nostr_sdk::prelude::*;
use std::str::FromStr;
use tauri::State;
use std::{fs::File, io::Write, str::FromStr};
use tauri::{Manager, State};
#[derive(serde::Serialize)]
pub struct CreateKeysResponse {
@ -24,14 +25,46 @@ pub fn create_keys() -> Result<CreateKeysResponse, ()> {
}
#[tauri::command]
pub fn get_public_key(nsec: String) -> Result<String, ()> {
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();
let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap();
let secret_key = keyring_entry.get_password().unwrap();
let app_key = age::x25519::Identity::from_str(&secret_key).unwrap();
let app_pubkey = app_key.to_public();
let config_dir = app_handle.path().app_config_dir().unwrap();
let encryptor =
age::Encryptor::with_recipients(vec![Box::new(app_pubkey)]).expect("we provided a recipient");
let file_ext = ".nsec".to_owned();
let file_path = nostr_npub + &file_ext;
let mut file = File::create(config_dir.join(file_path)).unwrap();
let mut writer = encryptor
.wrap_output(&mut file)
.expect("Init writer failed");
writer
.write_all(nsec.as_bytes())
.expect("Write nsec failed");
writer.finish().expect("Save nsec failed");
Ok(())
} else {
Err(())
}
}
#[tauri::command]
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"))
}
#[tauri::command]
pub async fn update_signer(nsec: String, nostr: State<'_, Nostr>) -> Result<(), ()> {
pub async fn update_signer(nsec: &str, nostr: State<'_, Nostr>) -> Result<(), ()> {
let client = &nostr.client;
let secret_key = SecretKey::from_bech32(nsec).unwrap();
let keys = Keys::new(secret_key);

View File

@ -1,113 +1,113 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "Lume",
"version": "3.0.1",
"identifier": "nu.lume.Lume",
"build": {
"beforeBuildCommand": "pnpm run build",
"beforeDevCommand": "pnpm desktop:dev",
"devUrl": "http://localhost:3000",
"frontendDist": "../dist"
},
"app": {
"macOSPrivateApi": true,
"withGlobalTauri": true,
"security": {
"assetProtocol": {
"enable": true,
"scope": [
"$APPDATA/*",
"$DATA/*",
"$LOCALDATA/*",
"$DESKTOP/*",
"$DOCUMENT/*",
"$DOWNLOAD/*",
"$HOME/*",
"$PICTURE/*",
"$PUBLIC/*",
"$VIDEO/*",
"$APPCONFIG/*",
"$RESOURCE/*"
]
}
},
"trayIcon": {
"iconPath": "icons/tray.png"
}
},
"bundle": {
"licenseFile": "../LICENSE",
"longDescription": "nostr client for desktop",
"shortDescription": "nostr client",
"targets": "all",
"active": true,
"category": "SocialNetworking",
"resources": ["resources/*", "./locales/*"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"appimage": {
"bundleMediaFramework": true,
"files": {}
},
"deb": {
"files": {}
},
"rpm": {
"epoch": 0,
"files": {},
"release": "1"
}
},
"macOS": {
"dmg": {
"appPosition": {
"x": 180,
"y": 170
},
"applicationFolderPosition": {
"x": 480,
"y": 170
},
"windowSize": {
"height": 400,
"width": 660
}
},
"files": {},
"minimumSystemVersion": "10.15"
},
"windows": {
"allowDowngrades": true,
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"nsis": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
"webviewInstallMode": {
"silent": true,
"type": "downloadBootstrapper"
},
"wix": null
}
},
"plugins": {
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3OTdCMkM3RjU5QzE2NzkKUldSNUZwejF4N0tYNTVHYjMrU0JkL090SlEyNUVLYU5TM2hTU3RXSWtEWngrZWJ4a0pydUhXZHEK",
"windows": {
"installMode": "quiet"
},
"endpoints": [
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
]
}
}
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "Lume",
"version": "3.0.1",
"identifier": "nu.lume.Lume",
"build": {
"beforeBuildCommand": "pnpm run build",
"beforeDevCommand": "pnpm desktop:dev",
"devUrl": "http://localhost:3000",
"frontendDist": "../dist"
},
"app": {
"macOSPrivateApi": true,
"withGlobalTauri": true,
"security": {
"assetProtocol": {
"enable": true,
"scope": [
"$APPDATA/*",
"$DATA/*",
"$LOCALDATA/*",
"$DESKTOP/*",
"$DOCUMENT/*",
"$DOWNLOAD/*",
"$HOME/*",
"$PICTURE/*",
"$PUBLIC/*",
"$VIDEO/*",
"$APPCONFIG/*",
"$RESOURCE/*"
]
}
},
"trayIcon": {
"iconPath": "icons/tray.png"
}
},
"bundle": {
"licenseFile": "../LICENSE",
"longDescription": "nostr client for desktop",
"shortDescription": "nostr client",
"targets": "all",
"active": true,
"category": "SocialNetworking",
"resources": ["resources/*", "./locales/*"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"appimage": {
"bundleMediaFramework": true,
"files": {}
},
"deb": {
"files": {}
},
"rpm": {
"epoch": 0,
"files": {},
"release": "1"
}
},
"macOS": {
"dmg": {
"appPosition": {
"x": 180,
"y": 170
},
"applicationFolderPosition": {
"x": 480,
"y": 170
},
"windowSize": {
"height": 400,
"width": 660
}
},
"files": {},
"minimumSystemVersion": "10.15"
},
"windows": {
"allowDowngrades": true,
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"nsis": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
"webviewInstallMode": {
"silent": true,
"type": "downloadBootstrapper"
},
"wix": null
}
},
"plugins": {
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3OTdCMkM3RjU5QzE2NzkKUldSNUZwejF4N0tYNTVHYjMrU0JkL090SlEyNUVLYU5TM2hTU3RXSWtEWngrZWJ4a0pydUhXZHEK",
"windows": {
"installMode": "quiet"
},
"endpoints": [
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
]
}
}
}