diff --git a/.gitignore b/.gitignore index 2031331b..3864ca7c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,9 @@ dist/ # Debug -*.log* +*.log.* # Misc .DS_Store *.pem +.vscode/ diff --git a/README.md b/README.md index 5f8881bc..1a1a8260 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,30 @@ -### Introduction +_Note_: Lume is under rewrite to using Rust Nostr as back-end and more lightweight front-end. If you need stable version, you can download v3 and below. -Lume is a nostr client +Source code for v3 is stored here: https://github.com/lumehq/lume/tree/old -### Usage +-- -Download Lume for your platform here: [https://github.com/lumehq/lume/releases](https://github.com/lumehq/lume/releases) +## Introduction + +Lume is a Nostr client for desktop include Linux, Windows and macOS. It is free and open source, you can look at source code on Github. Lume is actively improving the app and adding new features, you can expect new update every month. + +## Usage + +Download Lume v3 (v3.0.1-stable) for your platform here: [https://github.com/lumehq/lume/releases](https://github.com/lumehq/lume/releases) Supported platform: macOS, Windows and Linux -### Prerequisites +## Prerequisites -- PNPM or Bun (experiment) +- Node.js >= 18: https://nodejs.org/en -- Tauri: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-macos +- Rust: https://rustup.rs/ -### Develop +- PNPM: https://pnpm.io + +- Tauri v2: https://beta.tauri.app/guides/prerequisites/ + +## Develop Clone project @@ -40,7 +50,7 @@ Generate production build pnpm tauri build ``` -#### Nix +## Nix Requirements: @@ -53,8 +63,8 @@ Requirements: Copyright (C) 2023-2024 Ren Amamiya & other Lume contributors (see AUTHORS.md) -This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/. +You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/. diff --git a/apps/desktop/index.html b/apps/desktop/index.html deleted file mode 100644 index 39bb9c2f..00000000 --- a/apps/desktop/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Lume - - -
- - - diff --git a/apps/desktop/package.json b/apps/desktop/package.json deleted file mode 100644 index c8980376..00000000 --- a/apps/desktop/package.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "name": "lume", - "private": true, - "version": "3.0.0", - "scripts": { - "dev": "vite", - "build": "vite build" - }, - "dependencies": { - "@columns/antenas": "workspace:^", - "@columns/default": "workspace:^", - "@columns/foryou": "workspace:^", - "@columns/global": "workspace:^", - "@columns/group": "workspace:^", - "@columns/hashtag": "workspace:^", - "@columns/thread": "workspace:^", - "@columns/timeline": "workspace:^", - "@columns/trending-notes": "workspace:^", - "@columns/user": "workspace:^", - "@columns/waifu": "workspace:^", - "@getalby/sdk": "^3.2.3", - "@lume/ark": "workspace:^", - "@lume/icons": "workspace:^", - "@lume/storage": "workspace:^", - "@lume/ui": "workspace:^", - "@lume/utils": "workspace:^", - "@nostr-dev-kit/ndk": "^2.3.3", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-collapsible": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-hover-card": "^1.0.7", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tooltip": "^1.0.7", - "@tanstack/react-query": "^5.17.19", - "framer-motion": "^11.0.3", - "i18next": "^23.8.1", - "i18next-resources-to-backend": "^1.2.0", - "jotai": "^2.6.3", - "minidenticons": "^4.2.0", - "nanoid": "^5.0.4", - "nostr-fetch": "^0.15.0", - "nostr-tools": "^1.17.0", - "react": "^18.2.0", - "react-currency-input-field": "^3.6.14", - "react-dom": "^18.2.0", - "react-hook-form": "^7.49.3", - "react-i18next": "^14.0.1", - "react-router-dom": "^6.21.3", - "smol-toml": "^1.1.4", - "sonner": "^1.4.0", - "virtua": "^0.23.0" - }, - "devDependencies": { - "@lume/tailwindcss": "workspace:^", - "@lume/tsconfig": "workspace:^", - "@lume/types": "workspace:^", - "@types/node": "^20.11.10", - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.17", - "cross-env": "^7.0.3", - "encoding": "^0.1.13", - "postcss": "^8.4.33", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3", - "vite": "^5.0.12", - "vite-plugin-top-level-await": "^1.4.1", - "vite-tsconfig-paths": "^4.3.1" - } -} diff --git a/apps/desktop/postcss.config.js b/apps/desktop/postcss.config.js deleted file mode 100644 index e873f1a4..00000000 --- a/apps/desktop/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/apps/desktop/public/anime.jpg b/apps/desktop/public/anime.jpg deleted file mode 100644 index f7f8eeab..00000000 Binary files a/apps/desktop/public/anime.jpg and /dev/null differ diff --git a/apps/desktop/public/art.jpg b/apps/desktop/public/art.jpg deleted file mode 100644 index efc36ab3..00000000 Binary files a/apps/desktop/public/art.jpg and /dev/null differ diff --git a/apps/desktop/public/clapping_hands.png b/apps/desktop/public/clapping_hands.png deleted file mode 100644 index 593056a4..00000000 Binary files a/apps/desktop/public/clapping_hands.png and /dev/null differ diff --git a/apps/desktop/public/clown_face.png b/apps/desktop/public/clown_face.png deleted file mode 100644 index 3c650616..00000000 Binary files a/apps/desktop/public/clown_face.png and /dev/null differ diff --git a/apps/desktop/public/columns/antenas.jpg b/apps/desktop/public/columns/antenas.jpg deleted file mode 100644 index 7be430dd..00000000 Binary files a/apps/desktop/public/columns/antenas.jpg and /dev/null differ diff --git a/apps/desktop/public/columns/antenas@2x.jpg b/apps/desktop/public/columns/antenas@2x.jpg deleted file mode 100644 index 4b38bb86..00000000 Binary files a/apps/desktop/public/columns/antenas@2x.jpg and /dev/null differ diff --git a/apps/desktop/public/columns/global.jpg b/apps/desktop/public/columns/global.jpg deleted file mode 100644 index 2288bfaf..00000000 Binary files a/apps/desktop/public/columns/global.jpg and /dev/null differ diff --git a/apps/desktop/public/columns/global@2x.jpg b/apps/desktop/public/columns/global@2x.jpg deleted file mode 100644 index e079bfe1..00000000 Binary files a/apps/desktop/public/columns/global@2x.jpg and /dev/null differ diff --git a/apps/desktop/public/columns/group.jpg b/apps/desktop/public/columns/group.jpg deleted file mode 100644 index 49a01657..00000000 Binary files a/apps/desktop/public/columns/group.jpg and /dev/null differ diff --git a/apps/desktop/public/columns/group@2x.jpg b/apps/desktop/public/columns/group@2x.jpg deleted file mode 100644 index 3718db40..00000000 Binary files a/apps/desktop/public/columns/group@2x.jpg and /dev/null differ diff --git a/apps/desktop/public/columns/trending-notes.jpg b/apps/desktop/public/columns/trending-notes.jpg deleted file mode 100644 index 0ae75021..00000000 Binary files a/apps/desktop/public/columns/trending-notes.jpg and /dev/null differ diff --git a/apps/desktop/public/columns/trending-notes@2x.jpg b/apps/desktop/public/columns/trending-notes@2x.jpg deleted file mode 100644 index b99e5849..00000000 Binary files a/apps/desktop/public/columns/trending-notes@2x.jpg and /dev/null differ diff --git a/apps/desktop/public/columns/waifu.jpg b/apps/desktop/public/columns/waifu.jpg deleted file mode 100644 index 3af7c137..00000000 Binary files a/apps/desktop/public/columns/waifu.jpg and /dev/null differ diff --git a/apps/desktop/public/columns/waifu@2x.jpg b/apps/desktop/public/columns/waifu@2x.jpg deleted file mode 100644 index 67b4556a..00000000 Binary files a/apps/desktop/public/columns/waifu@2x.jpg and /dev/null differ diff --git a/apps/desktop/public/crying_face.png b/apps/desktop/public/crying_face.png deleted file mode 100644 index 47bfc717..00000000 Binary files a/apps/desktop/public/crying_face.png and /dev/null differ diff --git a/apps/desktop/public/face_with_open_mouth.png b/apps/desktop/public/face_with_open_mouth.png deleted file mode 100644 index 4b6f5c57..00000000 Binary files a/apps/desktop/public/face_with_open_mouth.png and /dev/null differ diff --git a/apps/desktop/public/face_with_tongue.png b/apps/desktop/public/face_with_tongue.png deleted file mode 100644 index b6bf2b8e..00000000 Binary files a/apps/desktop/public/face_with_tongue.png and /dev/null differ diff --git a/apps/desktop/public/fallback-image.jpg b/apps/desktop/public/fallback-image.jpg deleted file mode 100644 index 1490c0b7..00000000 Binary files a/apps/desktop/public/fallback-image.jpg and /dev/null differ diff --git a/apps/desktop/public/gaming.jpg b/apps/desktop/public/gaming.jpg deleted file mode 100644 index de58aeeb..00000000 Binary files a/apps/desktop/public/gaming.jpg and /dev/null differ diff --git a/apps/desktop/public/ghost.png b/apps/desktop/public/ghost.png deleted file mode 100644 index 8cf4548c..00000000 Binary files a/apps/desktop/public/ghost.png and /dev/null differ diff --git a/apps/desktop/public/icon.png b/apps/desktop/public/icon.png deleted file mode 100644 index 0e62e01a..00000000 Binary files a/apps/desktop/public/icon.png and /dev/null differ diff --git a/apps/desktop/public/movie.jpg b/apps/desktop/public/movie.jpg deleted file mode 100644 index bcb36809..00000000 Binary files a/apps/desktop/public/movie.jpg and /dev/null differ diff --git a/apps/desktop/public/music.jpg b/apps/desktop/public/music.jpg deleted file mode 100644 index 1c533c15..00000000 Binary files a/apps/desktop/public/music.jpg and /dev/null differ diff --git a/apps/desktop/public/nsfw.jpg b/apps/desktop/public/nsfw.jpg deleted file mode 100644 index f9e183d2..00000000 Binary files a/apps/desktop/public/nsfw.jpg and /dev/null differ diff --git a/apps/desktop/public/photography.jpg b/apps/desktop/public/photography.jpg deleted file mode 100644 index 6f117830..00000000 Binary files a/apps/desktop/public/photography.jpg and /dev/null differ diff --git a/apps/desktop/public/technology.jpg b/apps/desktop/public/technology.jpg deleted file mode 100644 index cfd733f0..00000000 Binary files a/apps/desktop/public/technology.jpg and /dev/null differ diff --git a/apps/desktop/public/translate.jpg b/apps/desktop/public/translate.jpg deleted file mode 100644 index 2b43e2d7..00000000 Binary files a/apps/desktop/public/translate.jpg and /dev/null differ diff --git a/apps/desktop/public/translate@2x.jpg b/apps/desktop/public/translate@2x.jpg deleted file mode 100644 index 8cfde295..00000000 Binary files a/apps/desktop/public/translate@2x.jpg and /dev/null differ diff --git a/apps/desktop/public/tutorial-1.gif b/apps/desktop/public/tutorial-1.gif deleted file mode 100644 index a536c39b..00000000 Binary files a/apps/desktop/public/tutorial-1.gif and /dev/null differ diff --git a/apps/desktop/public/tutorial-2.gif b/apps/desktop/public/tutorial-2.gif deleted file mode 100644 index 16f74d34..00000000 Binary files a/apps/desktop/public/tutorial-2.gif and /dev/null differ diff --git a/apps/desktop/public/tutorial-3.gif b/apps/desktop/public/tutorial-3.gif deleted file mode 100644 index 37ba9f6e..00000000 Binary files a/apps/desktop/public/tutorial-3.gif and /dev/null differ diff --git a/apps/desktop/public/zap.png b/apps/desktop/public/zap.png deleted file mode 100644 index d0747151..00000000 Binary files a/apps/desktop/public/zap.png and /dev/null differ diff --git a/apps/desktop/src/app.tsx b/apps/desktop/src/app.tsx deleted file mode 100644 index 4cb8af63..00000000 --- a/apps/desktop/src/app.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ColumnProvider, LumeProvider } from "@lume/ark"; -import { StorageProvider } from "@lume/storage"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { I18nextProvider } from "react-i18next"; -import { Toaster } from "sonner"; -import i18n from "./i18n"; -import Router from "./router"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // 10 seconds - }, - }, -}); - -export default function App() { - return ( - - - - - - - - - - - - - ); -} diff --git a/apps/desktop/src/main.jsx b/apps/desktop/src/main.jsx deleted file mode 100644 index 01e6aed0..00000000 --- a/apps/desktop/src/main.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import { createRoot } from "react-dom/client"; -import App from "./app"; -import "./app.css"; - -const container = document.getElementById("root"); -const root = createRoot(container); - -root.render(); diff --git a/apps/desktop/src/router.tsx b/apps/desktop/src/router.tsx deleted file mode 100644 index 23844d38..00000000 --- a/apps/desktop/src/router.tsx +++ /dev/null @@ -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: , - children: [ - { - path: "/", - element: , - errorElement: , - 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: , - 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: , - errorElement: , - 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 ( - - - - } - future={{ v7_startTransition: true }} - /> - ); -} diff --git a/apps/desktop/src/routes/activty/components/activityRepost.tsx b/apps/desktop/src/routes/activty/components/activityRepost.tsx deleted file mode 100644 index cc577bd0..00000000 --- a/apps/desktop/src/routes/activty/components/activityRepost.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { User } from "@lume/ark"; -import { NDKEvent } from "@nostr-dev-kit/ndk"; -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; - -export function ActivityRepost({ event }: { event: NDKEvent }) { - const { t } = useTranslation(); - - return ( - - - -
- -
- -

{t("activity.repost")}

-
-
- -
-
- - ); -} diff --git a/apps/desktop/src/routes/activty/components/activityText.tsx b/apps/desktop/src/routes/activty/components/activityText.tsx deleted file mode 100644 index 7a0e4cb3..00000000 --- a/apps/desktop/src/routes/activty/components/activityText.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { User } from "@lume/ark"; -import { NDKEvent } from "@nostr-dev-kit/ndk"; -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; - -export function ActivityText({ event }: { event: NDKEvent }) { - const { t } = useTranslation(); - - return ( - - - -
- -
- -

{t("activity.mention")}

-
-
- -
-
- - ); -} diff --git a/apps/desktop/src/routes/activty/components/activityZap.tsx b/apps/desktop/src/routes/activty/components/activityZap.tsx deleted file mode 100644 index 2085900f..00000000 --- a/apps/desktop/src/routes/activty/components/activityZap.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { User } from "@lume/ark"; -import { compactNumber } from "@lume/utils"; -import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk"; -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; - -export function ActivityZap({ event }: { event: NDKEvent }) { - const { t } = useTranslation(); - const invoice = zapInvoiceFromEvent(event); - - return ( - - - -
- -
- -

- {t("activity.zap")} {compactNumber.format(invoice.amount)} sats -

-
-
- -
-
- - ); -} diff --git a/apps/desktop/src/routes/activty/components/list.tsx b/apps/desktop/src/routes/activty/components/list.tsx deleted file mode 100644 index 02d6ff2f..00000000 --- a/apps/desktop/src/routes/activty/components/list.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useArk } from "@lume/ark"; -import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons"; -import { FETCH_LIMIT } from "@lume/utils"; -import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; -import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import { useCallback, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { ActivityRepost } from "./activityRepost"; -import { ActivityText } from "./activityText"; -import { ActivityZap } from "./activityZap"; - -export function ActivityList() { - const ark = useArk(); - const queryClient = useQueryClient(); - - const { t } = useTranslation(); - const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = - useInfiniteQuery({ - queryKey: ["activity"], - initialPageParam: 0, - queryFn: async ({ - signal, - pageParam, - }: { - signal: AbortSignal; - pageParam: number; - }) => { - const events = await ark.getInfiniteEvents({ - filter: { - kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap], - "#p": [ark.account.pubkey], - }, - limit: FETCH_LIMIT, - pageParam, - signal, - }); - - return events; - }, - getNextPageParam: (lastPage) => { - const lastEvent = lastPage.at(-1); - if (!lastEvent) return; - return lastEvent.created_at - 1; - }, - initialData: () => { - const queryCacheData = queryClient.getQueryState(["activity"]) - ?.data as NDKEvent[]; - if (queryCacheData) { - return { - pageParams: [undefined, 1], - pages: [queryCacheData], - }; - } - }, - staleTime: 360 * 1000, - refetchOnWindowFocus: false, - refetchOnMount: false, - }); - - const allEvents = useMemo( - () => (data ? data.pages.flatMap((page) => page) : []), - [data], - ); - - const renderEvenKind = useCallback( - (event: NDKEvent) => { - if (event.pubkey === ark.account.pubkey) return null; - switch (event.kind) { - case NDKKind.Text: - return ; - case NDKKind.Repost: - return ; - case NDKKind.Zap: - return ; - default: - return ; - } - }, - [data], - ); - - return ( -
- {isLoading ? ( -
- -
- ) : !allEvents.length ? ( -
-

🎉

-

{t("activity.empty")}

-
- ) : ( - allEvents.map((event) => renderEvenKind(event)) - )} -
- {hasNextPage ? ( - - ) : null} -
-
- ); -} diff --git a/apps/desktop/src/routes/activty/components/rootNote.tsx b/apps/desktop/src/routes/activty/components/rootNote.tsx deleted file mode 100644 index 5ba7e589..00000000 --- a/apps/desktop/src/routes/activty/components/rootNote.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Note, useEvent } from "@lume/ark"; - -export function ActivityRootNote({ eventId }: { eventId: string }) { - const { isLoading, isError, data } = useEvent(eventId); - - if (isLoading) { - return ( -
-
-
-
-
- ); - } - - if (isError) { - return ( -
-
- Failed to fetch event -
-
- ); - } - - return ( - - -
- -
- -
- -
-
- - - ); -} diff --git a/apps/desktop/src/routes/activty/components/singleRepost.tsx b/apps/desktop/src/routes/activty/components/singleRepost.tsx deleted file mode 100644 index c97e876b..00000000 --- a/apps/desktop/src/routes/activty/components/singleRepost.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { User } from "@lume/ark"; -import { NDKEvent } from "@nostr-dev-kit/ndk"; -import { useTranslation } from "react-i18next"; -import { ActivityRootNote } from "./rootNote"; - -export function ActivitySingleRepost({ event }: { event: NDKEvent }) { - const { t } = useTranslation(); - const repostId = event.tags.find((el) => el[0] === "e")[1]; - - return ( -
-
-

- {t("activity.boost")} -

-

- {t("activity.boostSubtitle")} -

-
-
-
- - - - - -
-
-

{t("activity.repost")}

-
-
- -
-
-
- ); -} diff --git a/apps/desktop/src/routes/activty/components/singleText.tsx b/apps/desktop/src/routes/activty/components/singleText.tsx deleted file mode 100644 index d0c6d734..00000000 --- a/apps/desktop/src/routes/activty/components/singleText.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Note, useArk } from "@lume/ark"; -import { NDKEvent } from "@nostr-dev-kit/ndk"; -import { useTranslation } from "react-i18next"; -import { ActivityRootNote } from "./rootNote"; - -export function ActivitySingleText({ event }: { event: NDKEvent }) { - const ark = useArk(); - const thread = ark.getEventThread({ - content: event.content, - tags: event.tags, - }); - - const { t } = useTranslation(); - - return ( -
-
-

- {t("activity.conversation")} -

-

- {t("activity.conversationSubtitle")} -

-
-
-
- {thread ? ( -
- {thread.rootEventId ? ( - - ) : null} - {thread.replyEventId ? ( - - ) : null} -
- ) : null} -
-
-

- {t("activity.newReply")} -

-
-
-
- - -
- -
- -
- -
-
- - -
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/activty/components/singleZap.tsx b/apps/desktop/src/routes/activty/components/singleZap.tsx deleted file mode 100644 index 59e25c42..00000000 --- a/apps/desktop/src/routes/activty/components/singleZap.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { User } from "@lume/ark"; -import { compactNumber } from "@lume/utils"; -import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk"; -import { ActivityRootNote } from "./rootNote"; - -export function ActivitySingleZap({ event }: { event: NDKEvent }) { - const zapEventId = event.tags.find((el) => el[0] === "e")[1]; - const invoice = zapInvoiceFromEvent(event); - - return ( -
-
-

- Conversation -

-

- @ Someone has replied to your note -

-
-
-
- - - - - -
-
-

- Zap you {compactNumber.format(invoice.amount)} sats for -

-
-
- -
-
-
- ); -} diff --git a/apps/desktop/src/routes/activty/id.tsx b/apps/desktop/src/routes/activty/id.tsx deleted file mode 100644 index 53350b55..00000000 --- a/apps/desktop/src/routes/activty/id.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useEvent } from "@lume/ark"; -import { LoaderIcon } from "@lume/icons"; -import { NDKKind } from "@nostr-dev-kit/ndk"; -import { useParams } from "react-router-dom"; -import { ActivitySingleRepost } from "./components/singleRepost"; -import { ActivitySingleText } from "./components/singleText"; -import { ActivitySingleZap } from "./components/singleZap"; - -export function ActivityIdScreen() { - const { id } = useParams(); - const { isLoading, data } = useEvent(id); - - if (isLoading || !data) { - return ( -
- -
- ); - } - - if (data.kind === NDKKind.Text) return ; - if (data.kind === NDKKind.Zap) return ; - if (data.kind === NDKKind.Repost) - return ; - - return ; -} diff --git a/apps/desktop/src/routes/activty/index.tsx b/apps/desktop/src/routes/activty/index.tsx deleted file mode 100644 index 1dc3e54c..00000000 --- a/apps/desktop/src/routes/activty/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { activityUnreadAtom } from "@lume/utils"; -import { useSetAtom } from "jotai"; -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { Outlet } from "react-router-dom"; -import { ActivityList } from "./components/list"; - -export function ActivityScreen() { - const { t } = useTranslation(); - const setUnreadActivity = useSetAtom(activityUnreadAtom); - - useEffect(() => { - setUnreadActivity(0); - }, []); - - return ( -
-
-
- {t("activity.title")} -
- -
-
- -
-
- ); -} diff --git a/apps/desktop/src/routes/auth/create-address.tsx b/apps/desktop/src/routes/auth/create-address.tsx deleted file mode 100644 index dcf8833a..00000000 --- a/apps/desktop/src/routes/auth/create-address.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { useArk } from "@lume/ark"; -import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { onboardingAtom } from "@lume/utils"; -import NDK, { - NDKEvent, - NDKKind, - NDKNip46Signer, - NDKPrivateKeySigner, -} from "@nostr-dev-kit/ndk"; -import * as Select from "@radix-ui/react-select"; -import { UnlistenFn } from "@tauri-apps/api/event"; -import { Window } from "@tauri-apps/api/window"; -import { useSetAtom } from "jotai"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { useLoaderData, useNavigate } from "react-router-dom"; -import { toast } from "sonner"; - -const Item = ({ event }: { event: NDKEvent }) => { - const domain = JSON.parse(event.content).nip05.replace("_@", ""); - - return ( - - @{domain} - - - - - ); -}; - -export function CreateAccountAddress() { - const ark = useArk(); - const storage = useStorage(); - const services = useLoaderData() as NDKEvent[]; - const setOnboarding = useSetAtom(onboardingAtom); - const navigate = useNavigate(); - - const [serviceId, setServiceId] = useState(services?.[0]?.id); - const [loading, setIsLoading] = useState(false); - - const { t } = useTranslation(); - const { - register, - handleSubmit, - formState: { isValid }, - } = useForm(); - - const getDomainName = (id: string) => { - const event = services.find((ev) => ev.id === id); - return JSON.parse(event.content).nip05.replace("_@", "") as string; - }; - - const onSubmit = async (data: { username: string; email: string }) => { - try { - setIsLoading(true); - - const domain = getDomainName(serviceId); - const service = services.find((ev) => ev.id === serviceId); - - // generate ndk for nsecbunker - const localSigner = NDKPrivateKeySigner.generate(); - const bunker = new NDK({ - explicitRelayUrls: [ - "wss://relay.nsecbunker.com/", - "wss://nostr.vulpem.com/", - ], - }); - await bunker.connect(2000); - - // generate tmp remote singer for create account - const remoteSigner = new NDKNip46Signer( - bunker, - service.pubkey, - localSigner, - ); - - // handle auth url request - let unlisten: UnlistenFn; - let authWindow: Window; - let account: string = undefined; - - remoteSigner.addListener("authUrl", async (authUrl: string) => { - authWindow = new Window(`auth-${serviceId}`, { - url: authUrl, - title: domain, - titleBarStyle: "overlay", - width: 600, - height: 650, - center: true, - closable: false, - }); - unlisten = await authWindow.onCloseRequested(() => { - if (!account) { - setIsLoading(false); - unlisten(); - - return authWindow.close(); - } - }); - }); - - // create new account - account = await remoteSigner.createAccount( - data.username, - domain, - data.email, - ); - - if (!account) { - unlisten(); - setIsLoading(false); - - authWindow.close(); - - return toast.error("Failed to create new account, try again later"); - } - - unlisten(); - authWindow.close(); - - // add account to storage - await storage.createSetting("nsecbunker", "1"); - const newAccount = await storage.createAccount({ - pubkey: account, - privkey: localSigner.privateKey, - }); - ark.account = newAccount; - - // get final signer with newly created account - const finalSigner = new NDKNip46Signer(bunker, account, localSigner); - await finalSigner.blockUntilReady(); - - // update main ndk instance signer - ark.updateNostrSigner({ signer: finalSigner }); - - // remove default nsecbunker profile and contact list - // await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] }); - await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] }); - - setIsLoading(false); - setOnboarding({ open: true, newUser: true }); - - return navigate("/auth/onboarding", { replace: true }); - } catch (e) { - setIsLoading(false); - toast.error(String(e)); - } - }; - - return ( -
-
-
-

- {t("signupWithProvider.title")} -

-
- {!services ? ( -
- -
- ) : ( -
-
-
- -
-
- - - - @{getDomainName(serviceId)} - - - - - - - - - - {t("signupWithProvider.chooseProvider")} - - {services.map((service) => ( - - ))} - - - - - -
- - {t("signupWithProvider.usernameFooter")} - -
-
-
-
- - -
- - {t("signupWithProvider.emailFooter")} - -
-
-
- -
-
- )} -
-
- ); -} diff --git a/apps/desktop/src/routes/auth/create-keys.tsx b/apps/desktop/src/routes/auth/create-keys.tsx deleted file mode 100644 index b0bd4358..00000000 --- a/apps/desktop/src/routes/auth/create-keys.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { useArk } from "@lume/ark"; -import { CheckIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { onboardingAtom } from "@lume/utils"; -import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; -import * as Checkbox from "@radix-ui/react-checkbox"; -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(); - - const [t] = useTranslation(); - const [key, setKey] = useState(""); - const [loading, setLoading] = useState(false); - const [showKey, setShowKey] = useState(false); - const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false }); - - const submit = async () => { - 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; - - setLoading(false); - setOnboarding({ open: true, newUser: true }); - - return navigate("/auth/onboarding", { replace: true }); - } catch (e) { - setLoading(false); - toast.error(String(e)); - } - }; - - useEffect(() => { - const privkey = NDKPrivateKeySigner.generate().privateKey; - setKey(nip19.nsecEncode(privkey)); - }, []); - - return ( -
-
-
-

- {t("signupWithSelfManage.title")} -

-

- {t("signupWithSelfManage.subtitle")} -

-
-
-
-
- - -
-
-
- - setConfirm((state) => ({ ...state, c1: !state.c1 })) - } - className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none" - id="confirm1" - > - - - - - -
-
- - setConfirm((state) => ({ ...state, c2: !state.c2 })) - } - className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none" - id="confirm2" - > - - - - - -
-
- - setConfirm((state) => ({ ...state, c3: !state.c3 })) - } - className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none" - id="confirm3" - > - - - - - -
-
-
- -
-
-
- ); -} diff --git a/apps/desktop/src/routes/auth/create.tsx b/apps/desktop/src/routes/auth/create.tsx deleted file mode 100644 index 5623022e..00000000 --- a/apps/desktop/src/routes/auth/create.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { LoaderIcon } from "@lume/icons"; -import { cn } from "@lume/utils"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; - -export function CreateAccountScreen() { - const navigate = useNavigate(); - - const [t] = useTranslation(); - const [method, setMethod] = useState<"self" | "managed">("self"); - const [loading, setLoading] = useState(false); - - const next = () => { - setLoading(true); - - if (method === "self") { - navigate("/auth/create-keys"); - } else { - navigate("/auth/create-address"); - } - }; - - return ( -
-
-
-

{t("signup.title")}

-

- {t("signup.subtitle")} -

-
-
- - -
- - {method === "managed" ? ( -
-

- Attention: -

-

- You're chosing Managed by Provider, this feature still in - "Beta". -

-

- Some functions still missing or not work as expected, you - shouldn't create your main account with this method -

- - Learn more - -
- ) : null} -
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/auth/login-key.tsx b/apps/desktop/src/routes/auth/login-key.tsx deleted file mode 100644 index 7de52444..00000000 --- a/apps/desktop/src/routes/auth/login-key.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useArk } from "@lume/ark"; -import { EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { getPublicKey, nip19 } from "nostr-tools"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { Trans, useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import { toast } from "sonner"; - -export function LoginWithKey() { - const ark = useArk(); - const storage = useStorage(); - const navigate = useNavigate(); - - const [showKey, setShowKey] = useState(false); - const [loading, setLoading] = useState(false); - - const { t } = useTranslation("loginWithPrivkey.subtitle"); - const { - register, - handleSubmit, - setError, - formState: { errors, isValid }, - } = useForm(); - - const onSubmit = async (data: { nsec: string }) => { - try { - if (!data.nsec.startsWith("nsec1")) - return toast.error("You need to enter a private key start with nsec1"); - - setLoading(true); - - const privkey = nip19.decode(data.nsec).data as string; - const pubkey = getPublicKey(privkey); - - const account = await storage.createAccount({ - pubkey: pubkey, - privkey: privkey, - }); - ark.account = account; - - return navigate("/auth/onboarding", { replace: true }); - } catch (e) { - setLoading(false); - setError("nsec", { - type: "manual", - message: String(e), - }); - } - }; - - return ( -
-
-
-

- {t("loginWithPrivkey.title")} -

-

- - Lume will put your private key to{" "} - - {storage.platform === "macos" - ? "Apple Keychain" - : storage.platform === "windows" - ? "Credential Manager" - : "Secret Service"} - - . It will be secured by your OS. - -

-
-
-
-
- - {errors.nsec && ( -

- {errors.nsec.message as string} -

- )} - -
- -
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/auth/login-nsecbunker.tsx b/apps/desktop/src/routes/auth/login-nsecbunker.tsx deleted file mode 100644 index fb38b39d..00000000 --- a/apps/desktop/src/routes/auth/login-nsecbunker.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; -import { nip19 } from "nostr-tools"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import { toast } from "sonner"; - -export function LoginWithNsecbunker() { - const ark = useArk(); - const storage = useStorage(); - const navigate = useNavigate(); - - const [loading, setLoading] = useState(false); - - const { t } = useTranslation(); - const { - register, - handleSubmit, - setError, - formState: { errors, isValid }, - } = useForm(); - - const onSubmit = async (data: { npub: string }) => { - try { - if (!data.npub.startsWith("npub1")) - return toast.info("You need to enter a token start with npub1"); - - if (!data.npub.includes("#")) - return toast.info("Token must include #secret"); - - setLoading(true); - - const bunker = new NDK({ - explicitRelayUrls: [ - "wss://relay.nsecbunker.com", - "wss://nostr.vulpem.com", - ], - }); - await bunker.connect(2000); - - const pubkey = nip19.decode(data.npub.split("#")[0]).data as string; - const localSigner = NDKPrivateKeySigner.generate(); - const remoteSigner = new NDKNip46Signer(bunker, data.npub, localSigner); - await remoteSigner.blockUntilReady(); - - ark.updateNostrSigner({ signer: remoteSigner }); - - await storage.createSetting("nsecbunker", "1"); - const account = await storage.createAccount({ - pubkey: pubkey, - privkey: localSigner.privateKey, - }); - ark.account = account; - - return navigate("/auth/onboarding", { replace: true }); - } catch (e) { - setLoading(false); - setError("npub", { - type: "manual", - message: String(e), - }); - } - }; - - return ( -
-
-
-

- {t("loginWithBunker.title")} -

-
-
-
-
- - {errors.npub && ( -

- {errors.npub.message as string} -

- )} -
- -
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/auth/login-oauth.tsx b/apps/desktop/src/routes/auth/login-oauth.tsx deleted file mode 100644 index 60981a82..00000000 --- a/apps/desktop/src/routes/auth/login-oauth.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { NIP05 } from "@lume/types"; -import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; -import { Window } from "@tauri-apps/api/window"; -import { fetch } from "@tauri-apps/plugin-http"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import { toast } from "sonner"; - -const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; - -export function LoginWithOAuth() { - const ark = useArk(); - const storage = useStorage(); - const navigate = useNavigate(); - - const [loading, setLoading] = useState(false); - - const { t } = useTranslation(); - const { - register, - handleSubmit, - setError, - formState: { errors, isValid }, - } = useForm(); - - const onSubmit = async (data: { nip05: string }) => { - try { - setLoading(true); - - if (!emailRegex.test(data.nip05)) { - setLoading(false); - return toast.error( - "Cannot verify your NIP-05 address, please try again later.", - ); - } - - const localPath = data.nip05.split("@")[0]; - const service = data.nip05.split("@")[1]; - - const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`; - - const req = await fetch(verifyURL, { - method: "GET", - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }); - - if (!req.ok) { - setLoading(false); - return toast.error( - "Cannot verify your NIP-05 address, please try again later.", - ); - } - - const res: NIP05 = await req.json(); - - if (!res.names[localPath.toLowerCase()] || !res.names[localPath]) { - setLoading(false); - return toast.error( - "Cannot verify your NIP-05 address, please try again later.", - ); - } - - const pubkey = - (res.names[localPath] as string) || - (res.names[localPath.toLowerCase()] as string); - - if (!res.nip46[pubkey]) { - setLoading(false); - return toast.error("Cannot found NIP-46 with this address"); - } - - const nip46Relays = res.nip46[pubkey] as unknown as string[]; - - const bunker = new NDK({ - explicitRelayUrls: nip46Relays || [ - "wss://relay.nsecbunker.com", - "wss://nostr.vulpem.com", - ], - }); - await bunker.connect(2000); - - const localSigner = NDKPrivateKeySigner.generate(); - const remoteSigner = new NDKNip46Signer(bunker, pubkey, localSigner); - - // handle auth url request - let authWindow: Window; - remoteSigner.addListener("authUrl", (authUrl: string) => { - authWindow = new Window(`auth-${pubkey}`, { - url: authUrl, - title: "Login", - titleBarStyle: "overlay", - width: 415, - height: 600, - center: true, - closable: false, - }); - }); - - const remoteUser = await remoteSigner.blockUntilReady(); - - if (remoteUser) { - authWindow.close(); - - ark.updateNostrSigner({ signer: remoteSigner }); - - await storage.createSetting("nsecbunker", "1"); - const account = await storage.createAccount({ - pubkey, - privkey: localSigner.privateKey, - }); - ark.account = account; - - return navigate("/auth/onboarding", { replace: true }); - } - } catch (e) { - setLoading(false); - setError("nip05", { - type: "manual", - message: String(e), - }); - } - }; - - return ( -
-
-
-

- {t("loginWithAddress.title")} -

-
-
-
-
- - {errors.nip05 && ( -

- {errors.nip05.message as string} -

- )} -
- -
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/auth/login.tsx b/apps/desktop/src/routes/auth/login.tsx deleted file mode 100644 index a08c9c84..00000000 --- a/apps/desktop/src/routes/auth/login.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; - -export function LoginScreen() { - const { t } = useTranslation(); - - return ( -
-
-
-

{t("login.title")}

-
-
-
- - {t("login.loginWithAddress")} - - - {t("login.loginWithBunker")} - -
-
-
-
-
-
-
- - {t("login.or")} - -
-
-
- - {t("login.loginWithPrivkey")} - -

- {t("login.footer")} -

-
-
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/auth/onboarding.tsx b/apps/desktop/src/routes/auth/onboarding.tsx deleted file mode 100644 index d6e8ca00..00000000 --- a/apps/desktop/src/routes/auth/onboarding.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { useArk } from "@lume/ark"; -import { InfoIcon, LoaderIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { TranslateRegisterModal } from "@lume/ui"; -import * as Switch from "@radix-ui/react-switch"; -import { - isPermissionGranted, - requestPermission, -} from "@tauri-apps/plugin-notification"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import { toast } from "sonner"; - -export function OnboardingScreen() { - const ark = useArk(); - const storage = useStorage(); - const navigate = useNavigate(); - - const [t] = useTranslation(); - const [loading, setLoading] = useState(false); - const [apiKey, setAPIKey] = useState(""); - const [settings, setSettings] = useState({ - notification: false, - lowPower: false, - translation: false, - }); - - const toggleLowPower = async () => { - await storage.createSetting("lowPower", String(+!settings.lowPower)); - setSettings((state) => ({ ...state, lowPower: !settings.lowPower })); - }; - - const toggleTranslation = async () => { - await storage.createSetting("translation", String(+!settings.translation)); - setSettings((state) => ({ ...state, translation: !settings.translation })); - }; - - const toggleNofitication = async () => { - await requestPermission(); - setSettings((state) => ({ - ...state, - notification: !settings.notification, - })); - }; - - const completeAuth = async () => { - if (settings.translation) { - if (!apiKey.length) - return toast.warning( - "You need to provide Translate API if enable translation", - ); - - await storage.createSetting("translateApiKey", apiKey); - } - - setLoading(true); - - // get account contacts - await ark.getUserContacts(); - - navigate("/", { replace: true }); - }; - - useEffect(() => { - async function loadSettings() { - // get notification permission - const permissionGranted = await isPermissionGranted(); - setSettings((prev) => ({ ...prev, notification: permissionGranted })); - - // get other settings - const data = await storage.getAllSettings(); - for (const item of data) { - if (item.key === "lowPower") - setSettings((prev) => ({ - ...prev, - lowPower: !!parseInt(item.value), - })); - - if (item.key === "translation") - setSettings((prev) => ({ - ...prev, - translation: !!parseInt(item.value), - })); - } - } - - loadSettings(); - }, []); - - return ( -
-
-
-

- {t("onboardingSettings.title")} -

-

- {t("onboardingSettings.subtitle")} -

-
-
-
- toggleNofitication()} - className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800" - > - - -
-

- {t("onboardingSettings.notification.title")} -

-

- {t("onboardingSettings.notification.subtitle")} -

-
-
-
- toggleLowPower()} - className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800" - > - - -
-

- {t("onboardingSettings.lowPower.title")} -

-

- {t("onboardingSettings.lowPower.subtitle")} -

-
-
-
- toggleTranslation()} - className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800" - > - - -
-

- {t("onboardingSettings.translation.title")} -

-

- {t("onboardingSettings.translation.subtitle")} -

-
-
- {settings.translation ? ( -
-

Translate API Key

- setAPIKey(e.target.value)} - className="w-full text-xl border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-11 rounded-lg ring-0 placeholder:text-neutral-600 bg-neutral-900" - /> -
-
-
-
-
-
- - Don't have an API key? - -
-
- -
-
- ) : null} -
- -

{t("onboardingSettings.footer")}

-
- -
-
-
- ); -} diff --git a/apps/desktop/src/routes/auth/welcome.tsx b/apps/desktop/src/routes/auth/welcome.tsx deleted file mode 100644 index 2ca0437a..00000000 --- a/apps/desktop/src/routes/auth/welcome.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; - -export function WelcomeScreen() { - const { t } = useTranslation(); - - return ( -
-
-
-
- lume -

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

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

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

-
-
- ); -} diff --git a/apps/desktop/src/routes/depot/components/contact.tsx b/apps/desktop/src/routes/depot/components/contact.tsx deleted file mode 100644 index eee12604..00000000 --- a/apps/desktop/src/routes/depot/components/contact.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon, RunIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { User } from "@lume/ui"; -import { NDKKind } from "@nostr-dev-kit/ndk"; -import { useState } from "react"; -import { toast } from "sonner"; - -export function DepotContactCard() { - const ark = useArk(); - const storage = useStorage(); - - const [status, setStatus] = useState(false); - - const backupContact = async () => { - try { - setStatus(true); - - const event = await ark.getEventByFilter({ - filter: { - authors: [ark.account.pubkey], - kinds: [NDKKind.Contacts], - }, - }); - - // broadcast to depot - const publish = await event.publish(); - - if (publish) { - setStatus(false); - toast.success("Backup contact list successfully."); - } - } catch (e) { - setStatus(false); - toast.error(String(e)); - } - }; - - return ( -
-
-
- {ark.account.contacts?.slice(0, 8).map((item) => ( - - ))} - {ark.account.contacts?.length > 8 ? ( -
- - +{ark.account.contacts?.length - 8} - -
- ) : null} -
-
-
-
Contacts
- -
-
- ); -} diff --git a/apps/desktop/src/routes/depot/components/members.tsx b/apps/desktop/src/routes/depot/components/members.tsx deleted file mode 100644 index d09802d8..00000000 --- a/apps/desktop/src/routes/depot/components/members.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { CancelIcon, PlusIcon, UserAddIcon, UserRemoveIcon } from "@lume/icons"; -import { User } from "@lume/ui"; -import * as Dialog from "@radix-ui/react-dialog"; -import { resolveResource, resolve } from "@tauri-apps/api/path"; -import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs"; -import { nip19 } from "nostr-tools"; -import { useEffect, useState } from "react"; -import { parse, stringify } from "smol-toml"; -import { toast } from "sonner"; -import { VITE_FLATPAK_RESOURCE } from "@lume/utils"; - -export function DepotMembers() { - const [members, setMembers] = useState>(null); - const [tmpMembers, setTmpMembers] = useState>([]); - const [newMember, setNewMember] = useState(""); - - const addMember = async () => { - if (!newMember.startsWith("npub1")) - return toast.error("You need to enter a valid npub"); - - try { - const pubkey = nip19.decode(newMember).data as string; - setTmpMembers((prev) => [...prev, pubkey]); - } catch (e) { - console.error(e); - } - }; - - const removeMember = (member: string) => { - setTmpMembers((prev) => prev.filter((item) => item !== member)); - }; - - const updateMembers = async () => { - setMembers(new Set(tmpMembers)); - - const defaultConfig = VITE_FLATPAK_RESOURCE !== null ? await resolve("/",VITE_FLATPAK_RESOURCE) : await resolveResource("resources/config.toml"); - const config = await readTextFile(defaultConfig); - const configContent = parse(config); - - // biome-ignore lint/complexity/useLiteralKeys: - configContent.authorization["pubkey_whitelist"] = [...members]; - - const newConfig = stringify(configContent); - - return await writeTextFile(defaultConfig, newConfig); - }; - - useEffect(() => { - async function loadConfig() { - const defaultConfig = VITE_FLATPAK_RESOURCE !== null ? await resolve("/",VITE_FLATPAK_RESOURCE) : await resolveResource("resources/config.toml"); - const config = await readTextFile(defaultConfig); - const configContent = parse(config); - setTmpMembers( - // biome-ignore lint/complexity/useLiteralKeys: - Array.from(configContent.authorization["pubkey_whitelist"]), - ); - } - - loadConfig(); - }, []); - - return ( - -
-
-

Members

-

- Only allowed users can publish event to your Depot -

-
-
-
- {tmpMembers.slice(0, 5).map((item) => ( - - ))} - {tmpMembers.length > 5 ? ( -
- - +{tmpMembers.length} - -
- ) : null} -
- - - Manage - -
-
- - - -
-
- - Manage member - -
- - - - -
-
-
-
- setNewMember(e.target.value)} - placeholder="npub1..." - className="h-11 w-full rounded-lg border-transparent bg-neutral-100 pl-3 pr-20 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" - /> - -
- {tmpMembers.map((member) => ( -
- - -
- ))} -
-
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/depot/components/profile.tsx b/apps/desktop/src/routes/depot/components/profile.tsx deleted file mode 100644 index a44c478a..00000000 --- a/apps/desktop/src/routes/depot/components/profile.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon, RunIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { User } from "@lume/ui"; -import { NDKKind } from "@nostr-dev-kit/ndk"; -import { useState } from "react"; -import { toast } from "sonner"; - -export function DepotProfileCard() { - const ark = useArk(); - const storage = useStorage(); - - const [status, setStatus] = useState(false); - - const backupProfile = async () => { - try { - setStatus(true); - - const event = await ark.getEventByFilter({ - filter: { - authors: [ark.account.pubkey], - kinds: [NDKKind.Metadata], - }, - }); - - // broadcast to depot - const publish = await event.publish(); - - if (publish) { - setStatus(false); - toast.success("Backup profile successfully."); - } - } catch (e) { - setStatus(false); - toast.error(JSON.stringify(e)); - } - }; - - return ( -
-
- -
-
-
Profile
- -
-
- ); -} diff --git a/apps/desktop/src/routes/depot/components/relays.tsx b/apps/desktop/src/routes/depot/components/relays.tsx deleted file mode 100644 index 0ef3cdee..00000000 --- a/apps/desktop/src/routes/depot/components/relays.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon, RunIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { NDKKind } from "@nostr-dev-kit/ndk"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; - -export function DepotRelaysCard() { - const ark = useArk(); - const storage = useStorage(); - - const [status, setStatus] = useState(false); - const [relaySize, setRelaySize] = useState(0); - - const backupRelays = async () => { - try { - setStatus(true); - - const event = await ark.getEventByFilter({ - filter: { - authors: [ark.account.pubkey], - kinds: [NDKKind.RelayList], - }, - }); - - // broadcast to depot - const publish = await event.publish(); - - if (publish) { - setStatus(false); - toast.success("Backup profile successfully."); - } - } catch (e) { - setStatus(false); - toast.error(JSON.stringify(e)); - } - }; - - useEffect(() => { - async function loadRelays() { - const event = await ark.getEventByFilter({ - filter: { - authors: [ark.account.pubkey], - kinds: [NDKKind.RelayList], - }, - }); - if (event) setRelaySize(event.tags.length); - } - - loadRelays(); - }, []); - - return ( -
-
-

{relaySize} relays

-
-
-
Relay List
- -
-
- ); -} diff --git a/apps/desktop/src/routes/depot/index.tsx b/apps/desktop/src/routes/depot/index.tsx deleted file mode 100644 index c9dac648..00000000 --- a/apps/desktop/src/routes/depot/index.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { useArk } from "@lume/ark"; -import { ChevronDownIcon, DepotIcon, GossipIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { NDKKind } from "@nostr-dev-kit/ndk"; -import * as Collapsible from "@radix-ui/react-collapsible"; -import { invoke } from "@tauri-apps/api/core"; -import { appConfigDir } from "@tauri-apps/api/path"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import { DepotContactCard } from "./components/contact"; -import { DepotMembers } from "./components/members"; -import { DepotProfileCard } from "./components/profile"; -import { DepotRelaysCard } from "./components/relays"; - -export function DepotScreen() { - const ark = useArk(); - const storage = useStorage(); - - const [dataPath, setDataPath] = useState(""); - const [tunnelUrl, setTunnelUrl] = useState(""); - - const openFolder = async () => { - await invoke("show_in_folder", { - path: `${dataPath}/nostr.db`, - }); - }; - - const updateRelayList = async () => { - try { - if (tunnelUrl.length < 1) - return toast.info("Please enter a valid relay url"); - if (!tunnelUrl.startsWith("ws")) - return toast.info("Please enter a valid relay url"); - - const relayUrl = new URL(tunnelUrl.replace(/\s/g, "")); - if (!/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(relayUrl.host)) return; - - const relayEvent = await ark.getEventByFilter({ - filter: { - authors: [ark.account.pubkey], - kinds: [NDKKind.RelayList], - }, - }); - - let publish: { id: string; seens: string[] }; - - if (!relayEvent) { - publish = await ark.createEvent({ - kind: NDKKind.RelayList, - tags: [["r", tunnelUrl, ""]], - }); - } - - const newTags = relayEvent.tags ?? []; - newTags.push(["r", tunnelUrl, ""]); - - publish = await ark.createEvent({ - kind: NDKKind.RelayList, - tags: newTags, - }); - - if (publish) { - await storage.createSetting("tunnel_url", tunnelUrl); - toast.success("Update relay list successfully."); - - setTunnelUrl(""); - } - } catch (e) { - console.error(e); - toast.error("Error"); - } - }; - - useEffect(() => { - async function loadConfig() { - const appDir = await appConfigDir(); - setDataPath(appDir); - } - - loadConfig(); - }, []); - - return ( -
-
-
-
-
- -
-
-

Depot is running

-
-
-
-
Relay URL
-
- ws://localhost:6090 -
-
-
-
Database
-
-

nostr.db (SQLite)

- -
-
-
-
-
-
-

- Actions -

-
-
- - -
-

Expose

-

- Make your Depot visible in the Internet, everyone can connect - into it. -

-
- -
- -
-
-

ngrok

- -
-
-

Cloudflare Tunnel

- -
-
-

Local Tunnel

- -
-
-
- -

- Support Gossip Model (Recommended) -

-
- -
-

- By adding to Relay List, other Nostr Client which support - Gossip Model will automatically connect to your Depot and - improve the discoverability. -

-
- setTunnelUrl(e.target.value)} - spellCheck={false} - placeholder="wss://" - className="h-10 flex-1 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" - /> - -
-
-
-
-
-
- - -
-

Backup (Recommended)

-

- Backup all your data to Depot, it always live on your machine. -

-
- -
- -
- - - -
-
-
- -
-
-
- ); -} diff --git a/apps/desktop/src/routes/depot/onboarding.tsx b/apps/desktop/src/routes/depot/onboarding.tsx deleted file mode 100644 index 95030d25..00000000 --- a/apps/desktop/src/routes/depot/onboarding.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon } from "@lume/icons"; -import { delay, VITE_FLATPAK_RESOURCE } from "@lume/utils"; -import { resolve, resolveResource } from "@tauri-apps/api/path"; -import { useStorage } from "@lume/storage"; -import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs"; -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { parse, stringify } from "smol-toml"; -import { toast } from "sonner"; - -export function DepotOnboardingScreen() { - const ark = useArk(); - const storage = useStorage(); - const navigate = useNavigate(); - - const [loading, setLoading] = useState(false); - - const launchDepot = async () => { - try { - setLoading(true); - - // get default config - const defaultConfig = - VITE_FLATPAK_RESOURCE !== null - ? await resolve("/", VITE_FLATPAK_RESOURCE) - : await resolveResource("resources/config.toml"); - const config = await readTextFile(defaultConfig); - const parsedConfig = parse(config); - - // add current user to whitelist - // biome-ignore lint/complexity/useLiteralKeys: - parsedConfig.authorization["pubkey_whitelist"].push(ark.account.pubkey); - - // update new config - const newConfig = stringify(parsedConfig); - await writeTextFile(defaultConfig, newConfig); - - // launch depot - await storage.launchDepot(); - await storage.createSetting("depot", "1"); - await delay(2000); // delay 2s to make sure depot is running - - // default depot url: ws://localhost:6090 - // #TODO: user can custom depot url - const connect = await ark.connectDepot(); - - if (connect) { - toast.success("Your Depot is successfully launch."); - setLoading(false); - - navigate("/depot/"); - } - } catch (e) { - toast.error(String(e)); - } - }; - - return ( -
-
-
-

- Run your Personal Nostr Relay inside Lume -

-

Your Relay, Your Control.

-
-
- -
-
-
- ); -} diff --git a/apps/desktop/src/routes/error.tsx b/apps/desktop/src/routes/error.tsx deleted file mode 100644 index 7ade36bb..00000000 --- a/apps/desktop/src/routes/error.tsx +++ /dev/null @@ -1,159 +0,0 @@ -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"; - -interface RouteError { - statusText: string; - message: string; -} - -export function ErrorScreen() { - const ark = useArk(); - const storage = useStorage(); - const error = useRouteError() as RouteError; - - const restart = async () => { - await relaunch(); - }; - - const download = async () => { - try { - const downloadPath = await downloadDir(); - const fileName = `nostr_keys_${new Date().toISOString()}.txt`; - const filePath = await save({ - defaultPath: `${downloadPath}/${fileName}`, - }); - const nsec = await storage.loadPrivkey(ark.account.pubkey); - - if (filePath) { - if (nsec) { - await writeTextFile( - filePath, - `Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}\nPrivate key: ${nsec}`, - ); - } else { - await writeTextFile( - filePath, - `Nostr account, generated by Lume (lume.nu)\nPublic key: ${ark.account.id}`, - ); - } - } // else { user cancel action } - } catch (e) { - await message(e, { - title: "Cannot download account keys", - type: "error", - }); - } - }; - - return ( -
-
-
-

- Sorry, an unexpected error has occurred. -

-

- Don't panic, your account is safe. -
- Here are what things you can do: -

-
-
-
-
- 1. Try to close and re-open the app -
- -
-
-
- 2. Backup Nostr account -
- -
-
-
-
-
- 3. Report this issue to Lume -
- - Report - -
-
-

- {error.statusText || error.message} -

-
-
-
-
-
-
- 4. Use another Nostr client -
-
-

- While waiting for Lume release the bug fixes, you always can - use other Nostr clients with your account: -

- -
-
-
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/home/index.tsx b/apps/desktop/src/routes/home/index.tsx deleted file mode 100644 index 17725662..00000000 --- a/apps/desktop/src/routes/home/index.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { Antenas } from "@columns/antenas"; -import { Default } from "@columns/default"; -import { ForYou } from "@columns/foryou"; -import { Global } from "@columns/global"; -import { Group } from "@columns/group"; -import { Hashtag } from "@columns/hashtag"; -import { Thread } from "@columns/thread"; -import { Timeline } from "@columns/timeline"; -import { TrendingNotes } from "@columns/trending-notes"; -import { User } from "@columns/user"; -import { Waifu } from "@columns/waifu"; -import { useColumnContext } from "@lume/ark"; -import { - ArrowLeftIcon, - ArrowRightIcon, - PlusIcon, - PlusSquareIcon, -} from "@lume/icons"; -import { IColumn } from "@lume/types"; -import { TutorialModal } from "@lume/ui/src/tutorial/modal"; -import { COL_TYPES } from "@lume/utils"; -import * as Tooltip from "@radix-ui/react-tooltip"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { VList } from "virtua"; - -export function HomeScreen() { - const { t } = useTranslation(); - const { columns, vlistRef, addColumn } = useColumnContext(); - - const [selectedIndex, setSelectedIndex] = useState(-1); - - const renderItem = (column: IColumn) => { - switch (column.kind) { - case COL_TYPES.default: - return ; - case COL_TYPES.newsfeed: - return ; - case COL_TYPES.foryou: - return ; - case COL_TYPES.thread: - return ; - case COL_TYPES.user: - return ; - case COL_TYPES.hashtag: - return ; - case COL_TYPES.group: - return ; - case COL_TYPES.antenas: - return ; - case COL_TYPES.global: - return ; - case COL_TYPES.trendingNotes: - return ; - case COL_TYPES.waifu: - return ; - default: - return ; - } - }; - - return ( -
- { - if (!vlistRef.current) return; - switch (e.code) { - case "ArrowUp": - case "ArrowLeft": { - e.preventDefault(); - const prevIndex = Math.max(selectedIndex - 1, 0); - setSelectedIndex(prevIndex); - vlistRef.current.scrollToIndex(prevIndex, { - align: "center", - smooth: true, - }); - break; - } - case "ArrowDown": - case "ArrowRight": { - e.preventDefault(); - const nextIndex = Math.min(selectedIndex + 1, columns.length - 1); - setSelectedIndex(nextIndex); - vlistRef.current.scrollToIndex(nextIndex, { - align: "center", - smooth: true, - }); - break; - } - default: - break; - } - }} - > - {columns.map((column) => renderItem(column))} -
- -
-
- -
-
- - - - - - - {t("global.moveLeft")} - - - - - - - - - - - {t("global.moveRight")} - - - - - - - - - - - {t("global.newColumn")} - - - - -
- -
-
- -
- ); -} diff --git a/apps/desktop/src/routes/relays/components/relayEventList.tsx b/apps/desktop/src/routes/relays/components/relayEventList.tsx deleted file mode 100644 index bc637764..00000000 --- a/apps/desktop/src/routes/relays/components/relayEventList.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { NoteSkeleton, RepostNote, TextNote, useArk } from "@lume/ark"; -import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons"; -import { FETCH_LIMIT } from "@lume/utils"; -import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { useCallback, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { VList } from "virtua"; - -export function RelayEventList({ relayUrl }: { relayUrl: string }) { - const ark = useArk(); - - const { t } = useTranslation(); - const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = - useInfiniteQuery({ - queryKey: ["relay-events", relayUrl], - initialPageParam: 0, - queryFn: async ({ - signal, - pageParam, - }: { - signal: AbortSignal; - pageParam: number; - }) => { - const url = `wss://${relayUrl}`; - const events = await ark.getRelayEvents({ - relayUrl: url, - filter: { - kinds: [NDKKind.Text, NDKKind.Repost], - }, - limit: FETCH_LIMIT, - pageParam, - signal, - }); - - return events; - }, - getNextPageParam: (lastPage) => { - const lastEvent = lastPage.at(-1); - if (!lastEvent) return; - return lastEvent.created_at - 1; - }, - select: (data) => data?.pages.flatMap((page) => page), - refetchOnWindowFocus: false, - }); - - const renderItem = useCallback( - (event: NDKEvent) => { - switch (event.kind) { - case NDKKind.Text: - return ; - case NDKKind.Repost: - return ; - default: - return ; - } - }, - [data], - ); - - return ( - - {status === "pending" ? ( - - ) : ( - data.map((item) => renderItem(item)) - )} -
- {hasNextPage ? ( - - ) : null} -
-
- ); -} diff --git a/apps/desktop/src/routes/relays/components/relayForm.tsx b/apps/desktop/src/routes/relays/components/relayForm.tsx deleted file mode 100644 index 27808861..00000000 --- a/apps/desktop/src/routes/relays/components/relayForm.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useRelaylist } from "@lume/ark"; -import { PlusIcon } from "@lume/icons"; -import { normalizeRelayUrl } from "nostr-fetch"; -import { useState } from "react"; -import { toast } from "sonner"; - -export function RelayForm() { - const { connectRelay } = useRelaylist(); - - const [relay, setRelay] = useState<{ - url: WebSocket["url"]; - purpose: "read" | "write" | undefined; - }>({ url: "", purpose: undefined }); - - const create = () => { - if (relay.url.length < 1) return toast.info("Please enter relay url"); - try { - const relayUrl = new URL(relay.url.replace(/\s/g, "")); - - if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") { - connectRelay.mutate(normalizeRelayUrl(relay.url)); - setRelay({ url: "", purpose: undefined }); - } else { - return toast.error( - "URL is invalid, a relay must use websocket protocol (start with wss:// or ws://). Please check again", - ); - } - } catch { - return toast.error("Relay URL is not valid. Please check again"); - } - }; - - return ( -
- setRelay((prev) => ({ ...prev, url: e.target.value }))} - /> - -
- ); -} diff --git a/apps/desktop/src/routes/relays/components/relayItem.tsx b/apps/desktop/src/routes/relays/components/relayItem.tsx deleted file mode 100644 index 001c73a2..00000000 --- a/apps/desktop/src/routes/relays/components/relayItem.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { User, useRelaylist } from "@lume/ark"; -import { PlusIcon, SearchIcon } from "@lume/icons"; -import { normalizeRelayUrl } from "nostr-fetch"; -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; - -export function RelayItem({ url, users }: { url: string; users?: string[] }) { - const domain = new URL(url).hostname; - - const { t } = useTranslation(); - const { connectRelay } = useRelaylist(); - - return ( -
-
- - {t("global.relay")}:{" "} - - - {url} - -
-
- {users ? ( -
- {users.slice(0, 4).map((item) => ( - - - - - - ))} - {users.length > 4 ? ( -
- +{users.length - 4} -
- ) : null} -
- ) : null} - - - {t("global.inspect")} - - -
-
- ); -} diff --git a/apps/desktop/src/routes/relays/components/sidebar.tsx b/apps/desktop/src/routes/relays/components/sidebar.tsx deleted file mode 100644 index b92c2381..00000000 --- a/apps/desktop/src/routes/relays/components/sidebar.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useArk, useRelaylist } from "@lume/ark"; -import { CancelIcon, LoaderIcon, RefreshIcon } from "@lume/icons"; -import { cn } from "@lume/utils"; -import { NDKKind, NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk"; -import { useQuery } from "@tanstack/react-query"; -import { useTranslation } from "react-i18next"; -import { RelayForm } from "./relayForm"; - -export function RelaySidebar({ className }: { className?: string }) { - const ark = useArk(); - - const { t } = useTranslation(); - const { removeRelay } = useRelaylist(); - const { status, data, isRefetching, refetch } = useQuery({ - queryKey: ["relay-personal"], - queryFn: async () => { - const event = await ark.getEventByFilter({ - filter: { - kinds: [NDKKind.RelayList], - authors: [ark.account.pubkey], - }, - cache: NDKSubscriptionCacheUsage.ONLY_RELAY, - }); - if (!event) return []; - return event.tags.filter((tag) => tag[0] === "r"); - }, - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - staleTime: Infinity, - }); - - const currentRelays = new Set( - ark.ndk.pool.connectedRelays().map((item) => item.url), - ); - - return ( -
-
-

{t("relays.sidebar.title")}

- -
-
- {status === "pending" ? ( -
- -
- ) : !data.length ? ( -
-

{t("relays.sidebar.empty")}

-
- ) : ( - data.map((item) => ( -
-
- {currentRelays.has(item[1]) ? ( - - - - - ) : ( - - - - - )} -

- {item[1] - .replace("wss://", "") - .replace("ws://", "") - .replace("/", "")} -

-
-
- {item[2]?.length ? ( -
- {item[2]} -
- ) : null} - -
-
- )) - )} - -
-
- ); -} diff --git a/apps/desktop/src/routes/relays/follows.tsx b/apps/desktop/src/routes/relays/follows.tsx deleted file mode 100644 index 6b1a31e4..00000000 --- a/apps/desktop/src/routes/relays/follows.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon } from "@lume/icons"; -import { useQuery } from "@tanstack/react-query"; -import { VList } from "virtua"; -import { RelayItem } from "./components/relayItem"; - -export function RelayFollowsScreen() { - const ark = useArk(); - const { - isLoading, - isError, - data: relays, - } = useQuery({ - queryKey: ["relay-follows"], - queryFn: async ({ signal }: { signal: AbortSignal }) => { - const data = await ark.getAllRelaysFromContacts({ signal }); - if (!data) throw new Error("Failed to get relay list from contacts"); - return data; - }, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }); - - if (isLoading) { - return ( -
- -
- ); - } - - if (isError || !relays) { - return ( -
-

Error

-
- ); - } - - return ( - - {[...relays].map(([key, value]) => ( - - ))} - - ); -} diff --git a/apps/desktop/src/routes/relays/global.tsx b/apps/desktop/src/routes/relays/global.tsx deleted file mode 100644 index dba51189..00000000 --- a/apps/desktop/src/routes/relays/global.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { LoaderIcon } from "@lume/icons"; -import { useQuery } from "@tanstack/react-query"; -import { fetch } from "@tauri-apps/plugin-http"; -import { VList } from "virtua"; -import { RelayItem } from "./components/relayItem"; - -export function RelayGlobalScreen() { - const { isLoading, data: relays } = useQuery({ - queryKey: ["relay-global"], - queryFn: async ({ signal }: { signal: AbortSignal }) => { - const res = await fetch("https://api.nostr.watch/v1/online", { signal }); - if (!res.ok) throw new Error("Failed to get online relays"); - return (await res.json()) as string[]; - }, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }); - - if (isLoading) { - return ( -
- -
- ); - } - - return ( - - {relays.map((item: string) => ( - - ))} - - ); -} diff --git a/apps/desktop/src/routes/relays/index.tsx b/apps/desktop/src/routes/relays/index.tsx deleted file mode 100644 index 7e9a7af1..00000000 --- a/apps/desktop/src/routes/relays/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { cn } from "@lume/utils"; -import { useTranslation } from "react-i18next"; -import { NavLink, Outlet } from "react-router-dom"; -import { RelaySidebar } from "./components/sidebar"; - -export function RelaysScreen() { - const { t } = useTranslation(); - - return ( -
- -
-
- - cn( - "h-9 w-24 rounded-lg inline-flex items-center justify-center font-medium", - isActive - ? "bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-950 dark:hover:bg-neutral-900" - : "", - ) - } - > - {t("relays.global")} - - - cn( - "h-9 w-24 rounded-lg inline-flex items-center justify-center font-medium", - isActive - ? "bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-950 dark:hover:bg-neutral-900" - : "", - ) - } - > - {t("relays.follows")} - -
-
- -
-
-
- ); -} diff --git a/apps/desktop/src/routes/relays/url.tsx b/apps/desktop/src/routes/relays/url.tsx deleted file mode 100644 index 7c5a7ee0..00000000 --- a/apps/desktop/src/routes/relays/url.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { LoaderIcon } from "@lume/icons"; -import { NIP11 } from "@lume/types"; -import { User } from "@lume/ui"; -import { Suspense } from "react"; -import { useTranslation } from "react-i18next"; -import { Await, useLoaderData, useParams } from "react-router-dom"; -import { RelayEventList } from "./components/relayEventList"; - -export function RelayUrlScreen() { - const { t } = useTranslation(); - const { url } = useParams(); - - const data: { relay?: { [key: string]: string } } = useLoaderData(); - - const getSoftwareName = (url: string) => { - const filename = url.substring(url.lastIndexOf("/") + 1); - return filename.replace(".git", ""); - }; - - const titleCase = (s: string) => { - return s - .replace(/^[-_]*(.)/, (_, c) => c.toUpperCase()) - .replace(/[-_]+(.)/g, (_, c) => ` ${c.toUpperCase()}`); - }; - - return ( -
-
- -
-
- - - {t("global.loading")} -
- } - > - -

{t("relays.relayView.empty")}

-
- } - > - {(resolvedRelay: NIP11) => ( -
-
-

{resolvedRelay.name}

-

- {resolvedRelay.description} -

-
- {resolvedRelay.pubkey ? ( -
-
- {t("relays.relayView.owner")}: -
-
- -
-
- ) : null} - {resolvedRelay.contact ? ( -
-
- {t("relays.relayView.contact")}: -
- - {resolvedRelay.contact} - -
- ) : null} - -
-
- {t("relays.relayView.nips")}: -
-
- {resolvedRelay.supported_nips.map((item) => ( - - {item} - - ))} -
-
- {resolvedRelay.limitation ? ( -
-
- {t("relays.relayView.limit")} -
-
- {Object.keys(resolvedRelay.limitation).map((key) => { - return ( -
-

- {titleCase(key)}: -

-

- {resolvedRelay.limitation[key].toString()} -

-
- ); - })} -
-
- ) : null} - {resolvedRelay.payments_url ? ( -
- - {t("relays.relayView.payment")} - - - {t("relays.relayView.paymentNote")} - -
- ) : null} -
- )} - - -
-
- ); -} diff --git a/apps/desktop/src/routes/settings/about.tsx b/apps/desktop/src/routes/settings/about.tsx deleted file mode 100644 index 5cbb23f5..00000000 --- a/apps/desktop/src/routes/settings/about.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { getVersion } from "@tauri-apps/api/app"; -import { relaunch } from "@tauri-apps/plugin-process"; -import { Update, check } from "@tauri-apps/plugin-updater"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; -import { toast } from "sonner"; - -export function AboutScreen() { - const [t] = useTranslation(); - const [version, setVersion] = useState(""); - const [newUpdate, setNewUpdate] = useState(null); - - const checkUpdate = async () => { - const update = await check(); - if (!update) toast.info("There is no update available"); - setNewUpdate(update); - }; - - const installUpdate = async () => { - await newUpdate.downloadAndInstall(); - await relaunch(); - }; - - useEffect(() => { - async function loadVersion() { - const appVersion = await getVersion(); - setVersion(appVersion); - } - - loadVersion(); - }, []); - - return ( -
-
-

Lume

-

- {t("settings.about.version")} {version} -

-
-
- {!newUpdate ? ( - - ) : ( - - )} - - Website - - - Report a issue - -
-
- ); -} diff --git a/apps/desktop/src/routes/settings/advanced.tsx b/apps/desktop/src/routes/settings/advanced.tsx deleted file mode 100644 index 710189e4..00000000 --- a/apps/desktop/src/routes/settings/advanced.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useStorage } from "@lume/storage"; -import { useTranslation } from "react-i18next"; - -export function AdvancedSettingScreen() { - const storage = useStorage(); - const { t } = useTranslation(); - - const clearCache = async () => { - await storage.clearCache(); - }; - - return ( -
-
-
-
-
- {t("settings.advanced.cache.title")} -
-
- {t("settings.advanced.cache.subtitle")} -
-
- -
-
-
- ); -} diff --git a/apps/desktop/src/routes/settings/backup.tsx b/apps/desktop/src/routes/settings/backup.tsx deleted file mode 100644 index aa095b44..00000000 --- a/apps/desktop/src/routes/settings/backup.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useArk } from "@lume/ark"; -import { EyeOffIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { nip19 } from "nostr-tools"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -export function BackupSettingScreen() { - const ark = useArk(); - const storage = useStorage(); - - const [t] = useTranslation(); - const [privkey, setPrivkey] = useState(null); - const [showPassword, setShowPassword] = useState(false); - - const removePrivkey = async () => { - await storage.removePrivkey(ark.account.pubkey); - }; - - useEffect(() => { - async function loadPrivkey() { - const key = await storage.loadPrivkey(ark.account.pubkey); - if (key) setPrivkey(key); - } - - loadPrivkey(); - }, []); - - return ( -
-
- {privkey ? ( -
-
- {t("settings.backup.privkey.title")} -
-
- - -
- -
- ) : null} -
-
- ); -} diff --git a/apps/desktop/src/routes/settings/components/avatarUpload.tsx b/apps/desktop/src/routes/settings/components/avatarUpload.tsx deleted file mode 100644 index 601f43f6..00000000 --- a/apps/desktop/src/routes/settings/components/avatarUpload.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon } from "@lume/icons"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; - -export function AvatarUpload({ setPicture }) { - const ark = useArk(); - - const [t] = useTranslation(); - const [loading, setLoading] = useState(false); - - const upload = async () => { - try { - setLoading(true); - - // upload image to nostr.build server - // #TODO: support multiple server - const image = await ark.upload({ fileExts: [] }); - - if (!image) - toast.error("Failed to upload image, please try again later."); - - setPicture(image); - setLoading(false); - } catch (e) { - setLoading(false); - toast.error("Failed to upload image, please try again later."); - } - }; - - return ( - - ); -} diff --git a/apps/desktop/src/routes/settings/components/coverUpload.tsx b/apps/desktop/src/routes/settings/components/coverUpload.tsx deleted file mode 100644 index bdeb7868..00000000 --- a/apps/desktop/src/routes/settings/components/coverUpload.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useArk } from "@lume/ark"; -import { LoaderIcon } from "@lume/icons"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; - -export function CoverUpload({ setBanner }) { - const ark = useArk(); - - const [t] = useTranslation(); - const [loading, setLoading] = useState(false); - - const upload = async () => { - try { - setLoading(true); - - // upload image to nostr.build server - // #TODO: support multiple server - const image = await ark.upload({ fileExts: [] }); - - if (!image) - toast.error("Failed to upload image, please try again later."); - - setBanner(image); - setLoading(false); - } catch (e) { - setLoading(false); - toast.error("Failed to upload image, please try again later."); - } - }; - - return ( - - ); -} diff --git a/apps/desktop/src/routes/settings/general.tsx b/apps/desktop/src/routes/settings/general.tsx deleted file mode 100644 index c9b4d699..00000000 --- a/apps/desktop/src/routes/settings/general.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import { DarkIcon, LightIcon, SystemModeIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; -import { cn } from "@lume/utils"; -import * as Switch from "@radix-ui/react-switch"; -import { invoke } from "@tauri-apps/api/core"; -import { getCurrent } from "@tauri-apps/api/window"; -import { disable, enable, isEnabled } from "@tauri-apps/plugin-autostart"; -import { - isPermissionGranted, - requestPermission, -} from "@tauri-apps/plugin-notification"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -export function GeneralSettingScreen() { - const storage = useStorage(); - - const [t] = useTranslation(); - const [apiKey, setAPIKey] = useState(""); - const [settings, setSettings] = useState({ - ...storage.settings, - notification: false, - autolaunch: false, - appearance: "system", - }); - - const changeTheme = async (theme: "light" | "dark" | "auto") => { - await invoke("plugin:theme|set_theme", { theme }); - // update state - setSettings((prev) => ({ ...prev, appearance: theme })); - }; - - const toggleLowPower = async () => { - await storage.createSetting("lowPower", String(+!settings.lowPower)); - setSettings((state) => ({ ...state, lowPower: !settings.lowPower })); - }; - - const toggleAutolaunch = async () => { - if (!settings.autolaunch) { - await enable(); - // update state - setSettings((prev) => ({ ...prev, autolaunch: true })); - } else { - await disable(); - // update state - setSettings((prev) => ({ ...prev, autolaunch: false })); - } - }; - - const toggleMedia = async () => { - await storage.createSetting("media", String(+!settings.media)); - storage.settings.media = !settings.media; - // update state - setSettings((prev) => ({ ...prev, media: !settings.media })); - }; - - const toggleHashtag = async () => { - await storage.createSetting("hashtag", String(+!settings.hashtag)); - storage.settings.hashtag = !settings.hashtag; - // update state - setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag })); - }; - - const toggleAutoupdate = async () => { - await storage.createSetting("autoupdate", String(+!settings.autoupdate)); - storage.settings.autoupdate = !settings.autoupdate; - // update state - setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate })); - }; - - const toggleNofitication = async () => { - if (settings.notification) return; - - await requestPermission(); - // update state - setSettings((prev) => ({ ...prev, notification: !settings.notification })); - }; - - const toggleTranslation = async () => { - await storage.createSetting("translation", String(+!settings.translation)); - storage.settings.translation = !settings.translation; - // update state - setSettings((prev) => ({ ...prev, translation: !settings.translation })); - }; - - const saveApi = async () => { - await storage.createSetting("translateApiKey", apiKey); - }; - - useEffect(() => { - async function loadSettings() { - const theme = await getCurrent().theme(); - setSettings((prev) => ({ ...prev, appearance: theme })); - - const autostart = await isEnabled(); - setSettings((prev) => ({ ...prev, autolaunch: autostart })); - - const permissionGranted = await isPermissionGranted(); - setSettings((prev) => ({ ...prev, notification: permissionGranted })); - } - - loadSettings(); - }, []); - - return ( -
-
-
-
-
- {t("settings.general.update.title")} -
-
- {t("settings.general.update.subtitle")} -
-
- toggleAutoupdate()} - className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800" - > - - -
-
-
-
- {t("settings.general.lowPower.title")} -
-
- {t("settings.general.lowPower.subtitle")} -
-
- toggleLowPower()} - className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800" - > - - -
-
-
-
- {t("settings.general.startup.title")} -
-
- {t("settings.general.startup.subtitle")} -
-
- toggleAutolaunch()} - className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800" - > - - -
-
-
-
- {t("settings.general.media.title")} -
-
- {t("settings.general.media.subtitle")} -
-
- toggleMedia()} - className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800" - > - - -
-
-
-
- {t("settings.general.hashtag.title")} -
-
- {t("settings.general.hashtag.subtitle")} -
-
- toggleHashtag()} - className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800" - > - - -
-
-
-
- {t("settings.general.notification.title")} -
-
- {t("settings.general.notification.subtitle")} -
-
- toggleNofitication()} - className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800" - > - - -
-
-
-
- {t("settings.general.translation.title")} -
-
- {t("settings.general.translation.subtitle")} -
-
- toggleTranslation()} - className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800" - > - - -
- {settings.translation ? ( -
-
- {t("global.apiKey")} -
-
- setAPIKey(e.target.value)} - className="w-full border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-9 rounded-lg ring-0 placeholder:text-neutral-600 bg-neutral-100 dark:bg-neutral-900" - /> -
- -
-
-
- ) : null} -
-
- {t("settings.general.appearance.title")} -
-
- - - -
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/settings/nwc.tsx b/apps/desktop/src/routes/settings/nwc.tsx deleted file mode 100644 index 114e8644..00000000 --- a/apps/desktop/src/routes/settings/nwc.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { useArk } from "@lume/ark"; -import { useStorage } from "@lume/storage"; -import * as Switch from "@radix-ui/react-switch"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; - -export function NWCScreen() { - const ark = useArk(); - const storage = useStorage(); - - const [t] = useTranslation(); - const [settings, setSettings] = useState({ - nwc: false, - instantZap: storage.settings.instantZap, - }); - const [walletConnectURL, setWalletConnectURL] = useState(null); - const [amount, setAmount] = useState("21"); - - const saveNWC = async () => { - try { - if (!walletConnectURL.startsWith("nostr+walletconnect:")) { - return toast.error( - "Connect URI is required and must start with format nostr+walletconnect:, please check again", - ); - } - - const uriObj = new URL(walletConnectURL); - const params = new URLSearchParams(uriObj.search); - - if (params.has("relay") && params.has("secret")) { - await storage.createPrivkey( - `${ark.account.pubkey}.nwc`, - walletConnectURL, - ); - - storage.nwc = walletConnectURL; - - setWalletConnectURL(walletConnectURL); - setSettings((state) => ({ ...state, nwc: true })); - } else { - return toast.error("Connect URI is not valid, please check again"); - } - } catch (e) { - toast.error(String(e)); - } - }; - - const toggleInstantZap = async () => { - await storage.createSetting("instantZap", String(+!settings.instantZap)); - setSettings((state) => ({ ...state, instantZap: !settings.instantZap })); - }; - - const saveAmount = async () => { - await storage.createSetting("zapAmount", amount); - }; - - const remove = async () => { - await storage.removePrivkey(`${ark.account.pubkey}.nwc`); - - setWalletConnectURL(""); - setSettings((state) => ({ ...state, nwc: false })); - storage.nwc = null; - }; - - useEffect(() => { - if (storage.nwc) { - setSettings((state) => ({ ...state, nwc: true })); - setWalletConnectURL(storage.nwc); - } - }, []); - - return ( -
-
-
-
-
- {t("settings.zap.nwc")} -
-
-