diff --git a/apps/desktop2/src/components/login.tsx b/apps/desktop2/src/components/login.tsx index 0d8cf6f2..9d644512 100644 --- a/apps/desktop2/src/components/login.tsx +++ b/apps/desktop2/src/components/login.tsx @@ -11,19 +11,15 @@ export function LoginDialog() { const [nsec, setNsec] = useState(""); const [passphase, setPassphase] = useState(""); - const [loading, setLoading] = useState(false); const login = async () => { try { - setLoading(true); - const save = await ark.save_account(nsec, passphase); if (save) { navigate({ to: "/", search: { guest: false } }); } } catch (e) { - setLoading(false); toast.error(String(e)); } }; diff --git a/apps/desktop2/src/routes/nwc.lazy.tsx b/apps/desktop2/src/routes/nwc.lazy.tsx new file mode 100644 index 00000000..ade7af10 --- /dev/null +++ b/apps/desktop2/src/routes/nwc.lazy.tsx @@ -0,0 +1,62 @@ +import { useArk } from "@lume/ark"; +import { ArrowRightIcon, ZapIcon } from "@lume/icons"; +import { Container } from "@lume/ui"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { useState } from "react"; + +export const Route = createLazyFileRoute("/nwc")({ + component: Screen, +}); + +function Screen() { + const ark = useArk(); + + const [uri, setUri] = useState(""); + const [isDone, setIsDone] = useState(false); + + const save = async () => { + const nwc = await ark.set_nwc(uri); + + if (nwc) { + setIsDone(true); + } + }; + + return ( + + + + + + + + + Connect bitcoin wallet to + start zapping to your favorite content and creator. + + + + + + Paste a Nostr Wallet Connect connection string + setUri(e.target.value)} + placeholder="nostrconnect://" + className="h-24 w-full resize-none rounded-lg border-transparent bg-white placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-black dark:focus:ring-blue-900" + /> + + + + Save & Connect + + + + + + ); +} diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index bc79b58e..19bcf928 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -409,6 +409,15 @@ export class Ark { } } + public async nwc_status() { + try { + const cmd: boolean = await invoke("nwc_status"); + return cmd; + } catch { + return false; + } + } + public async zap_profile(id: string, amount: number, message: string) { try { const cmd: boolean = await invoke("zap_profile", { id, amount, message }); @@ -526,4 +535,21 @@ export class Ark { fileDropEnabled: true, }); } + + public open_nwc() { + return new WebviewWindow("nwc", { + title: "Nostr Wallet Connect", + url: "/nwc", + minWidth: 400, + width: 400, + height: 600, + hiddenTitle: true, + titleBarStyle: "overlay", + fileDropEnabled: true, + }); + } + + public open_zap() { + // todo + } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 8cf1c4b6..8a61e059 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -33,7 +33,6 @@ "react-hook-form": "^7.51.0", "react-hotkeys-hook": "^4.5.0", "react-i18next": "^14.0.5", - "react-router-dom": "^6.22.2", "react-string-replace": "^1.1.1", "slate": "^0.101.5", "slate-react": "^0.101.6", diff --git a/packages/ui/src/column/content.tsx b/packages/ui/src/column/content.tsx deleted file mode 100644 index 09d2060e..00000000 --- a/packages/ui/src/column/content.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { ReactNode } from "react"; -import { Routes } from "react-router-dom"; - -export function ColumnContent({ children }: { children: ReactNode }) { - return {children}; -} diff --git a/packages/ui/src/column/header.tsx b/packages/ui/src/column/header.tsx deleted file mode 100644 index 9e9c8f7c..00000000 --- a/packages/ui/src/column/header.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { - ChevronDownIcon, - MoveLeftIcon, - MoveRightIcon, - RefreshIcon, - TrashIcon, -} from "@lume/icons"; -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; -import { useQueryClient } from "@tanstack/react-query"; -import { useTranslation } from "react-i18next"; - -export function ColumnHeader({ queryKey }: { queryKey?: string[] }) { - const { t } = useTranslation(); - const queryClient = useQueryClient(); - - const refresh = async () => { - if (queryKey) await queryClient.refetchQueries({ queryKey }); - }; - - return ( - - - - - {column.title} - - - - - - - - - {t("global.refresh")} - - - - move(column.id, "left")} - className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" - > - - {t("global.moveLeft")} - - - - move(column.id, "right")} - className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" - > - - {t("global.moveRight")} - - - - - remove(column.id)} - className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-red-500 hover:bg-red-500 hover:text-red-50 focus:outline-none" - > - - {t("global.delete")} - - - - - - - ); -} diff --git a/packages/ui/src/column/index.ts b/packages/ui/src/column/index.ts deleted file mode 100644 index 6b6813b8..00000000 --- a/packages/ui/src/column/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Route } from "react-router-dom"; -import { ColumnContent } from "./content"; -import { ColumnHeader } from "./header"; -import { ColumnLiveWidget } from "./live"; -import { ColumnProvider } from "./provider"; -import { ColumnRoot } from "./root"; - -export const Column = { - Provider: ColumnProvider, - Root: ColumnRoot, - Live: ColumnLiveWidget, - Header: ColumnHeader, - Content: ColumnContent, - Route: Route, -}; diff --git a/packages/ui/src/column/live.tsx b/packages/ui/src/column/live.tsx deleted file mode 100644 index 09118ebe..00000000 --- a/packages/ui/src/column/live.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { ArrowUpIcon } from "@lume/icons"; -import { useEffect, useState } from "react"; - -export function ColumnLiveWidget({ - filter, - onClick, -}: { - filter: NDKFilter; - onClick: (event: NDKEvent[]) => void; -}) { - const ark = useArk(); - const [events, setEvents] = useState([]); - - const update = async () => { - onClick(events); - // reset - setEvents([]); - }; - - useEffect(() => { - const sub = ark.subscribe({ - filter, - closeOnEose: false, - cb: (event: NDKEvent) => setEvents((prev) => [...prev, event]), - }); - - return () => { - if (sub) sub.stop(); - }; - }, []); - - if (!events.length) return null; - - return ( - - - {events.length} {events.length === 1 ? "new note" : "new notes"} - - - - ); -} diff --git a/packages/ui/src/column/provider.tsx b/packages/ui/src/column/provider.tsx deleted file mode 100644 index d37d5cd3..00000000 --- a/packages/ui/src/column/provider.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { LumeColumn } from "@lume/types"; -import { ReactNode, createContext, useContext } from "react"; - -const ColumnContext = createContext(null); - -export function ColumnProvider({ - column, - children, -}: { column: LumeColumn; children: ReactNode }) { - return ( - {children} - ); -} - -export function useColumnContext() { - const context = useContext(ColumnContext); - if (!context) { - throw new Error( - "Please import Column Provider to use useColumnContext() hook", - ); - } - return context; -} diff --git a/packages/ui/src/column/root.tsx b/packages/ui/src/column/root.tsx deleted file mode 100644 index bcafbb5f..00000000 --- a/packages/ui/src/column/root.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { cn } from "@lume/utils"; -import { Resizable } from "re-resizable"; -import { ReactNode, useState } from "react"; -import { MemoryRouter, UNSAFE_LocationContext } from "react-router-dom"; - -export function ColumnRoot({ - children, - className, -}: { - children: ReactNode; - className?: string; -}) { - const [width, setWidth] = useState(420); - - return ( - - e.preventDefault()} - onResizeStop={(_e, _direction, _ref, d) => { - setWidth((prevWidth) => prevWidth + d.width); - }} - minWidth={420} - maxWidth={600} - className={cn( - "relative flex flex-col border-r-2 border-neutral-50 hover:border-neutral-100 dark:border-neutral-950 dark:hover:border-neutral-900", - className, - )} - enable={{ right: true }} - > - - {children} - - - - ); -} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 566e7dcb..8ab1ac2d 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,6 +1,5 @@ export * from "./user"; export * from "./note"; -export * from "./column"; // UI export * from "./container"; diff --git a/packages/ui/src/note/buttons/zap.tsx b/packages/ui/src/note/buttons/zap.tsx index 3d00d580..3e3c193e 100644 --- a/packages/ui/src/note/buttons/zap.tsx +++ b/packages/ui/src/note/buttons/zap.tsx @@ -1,9 +1,22 @@ +import { useArk } from "@lume/ark"; import { ZapIcon } from "@lume/icons"; export function NoteZap() { + const ark = useArk(); + + const zap = async () => { + const nwc = await ark.nwc_status(); + if (!nwc) { + ark.open_nwc(); + } else { + ark.open_zap(); + } + }; + return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9993e337..62c91ae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -935,9 +935,6 @@ importers: react-i18next: specifier: ^14.0.5 version: 14.0.5(i18next@23.10.0)(react-dom@18.2.0)(react@18.2.0) - react-router-dom: - specifier: ^6.22.2 - version: 6.22.2(react-dom@18.2.0)(react@18.2.0) react-string-replace: specifier: ^1.1.1 version: 1.1.1 @@ -2989,11 +2986,6 @@ packages: '@babel/runtime': 7.24.0 dev: false - /@remix-run/router@1.15.2: - resolution: {integrity: sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==} - engines: {node: '>=14.0.0'} - dev: false - /@rollup/plugin-virtual@3.0.2: resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} engines: {node: '>=14.0.0'} @@ -6337,29 +6329,6 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.61)(react@18.2.0) dev: false - /react-router-dom@6.22.2(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' - dependencies: - '@remix-run/router': 1.15.2 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-router: 6.22.2(react@18.2.0) - dev: false - - /react-router@6.22.2(react@18.2.0): - resolution: {integrity: sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: '>=16.8' - dependencies: - '@remix-run/router': 1.15.2 - react: 18.2.0 - dev: false - /react-string-replace@1.1.1: resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==} engines: {node: '>=0.12.0'} diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json index ac6f984e..3d7e447d 100644 --- a/src-tauri/capabilities/main.json +++ b/src-tauri/capabilities/main.json @@ -8,6 +8,8 @@ "splash", "editor", "settings", + "nwc", + "zap", "event-*", "user-*", "column-*" diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index a9eaf54c..7fc969d9 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","editor","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","dialog:allow-open","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}} \ No newline at end of file +{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","editor","settings","nwc","zap","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","dialog:allow-open","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 91b54505..0b6c3a2f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -103,6 +103,7 @@ fn main() { nostr::metadata::get_settings, nostr::metadata::get_nwc_status, nostr::metadata::set_nwc, + nostr::metadata::nwc_status, nostr::metadata::zap_profile, nostr::metadata::zap_event, nostr::event::get_event, diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index 3169530b..8b71a2c9 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -1,4 +1,5 @@ use crate::Nostr; +use keyring::Entry; use nostr_sdk::prelude::*; use std::{str::FromStr, time::Duration}; use tauri::State; @@ -285,18 +286,30 @@ pub async fn get_nwc_status(state: State<'_, Nostr>) -> Result { pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; - if let Ok(uri) = NostrWalletConnectURI::from_str(&uri) { - if let Ok(nwc) = NWC::new(uri).await { + if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(&uri) { + if let Ok(nwc) = NWC::new(nwc_uri).await { + let keyring = Entry::new("Lume Secret Storage", "NWC").unwrap(); + let _ = keyring.set_password(uri); let _ = client.set_zapper(nwc); + Ok(true) } else { - Ok(false) + Err("URI is not valid".into()) } } else { Err("Set NWC failed".into()) } } +#[tauri::command] +pub async fn nwc_status(state: State<'_, Nostr>) -> Result { + let client = &state.client; + match client.zapper().await { + Ok(_) => Ok(true), + Err(_) => Err(false), + } +} + #[tauri::command] pub async fn zap_profile( id: &str,