feat: add zap for multi accounts

This commit is contained in:
reya 2024-10-22 15:59:23 +07:00
parent 9c61d6cad2
commit 64b78fff7f
15 changed files with 233 additions and 244 deletions

View File

@ -178,13 +178,13 @@ pub async fn has_signer(id: String, state: State<'_, Nostr>) -> Result<bool, Str
match client.signer().await { match client.signer().await {
Ok(signer) => { Ok(signer) => {
let signer_key = signer.public_key().await.unwrap(); // Emit reload in front-end
// handle.emit("signer", ()).unwrap();
if signer_key == public_key { let signer_key = signer.public_key().await.map_err(|e| e.to_string())?;
Ok(true) let is_match = signer_key == public_key;
} else {
Ok(false) Ok(is_match)
}
} }
Err(_) => Ok(false), Err(_) => Ok(false),
} }

View File

@ -449,7 +449,9 @@ pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, Stri
if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) { if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) {
let nwc = NWC::new(nwc_uri); let nwc = NWC::new(nwc_uri);
let keyring = Entry::new("Lume Secret", "Bitcoin Connect").map_err(|e| e.to_string())?; let keyring =
Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?;
keyring.set_password(uri).map_err(|e| e.to_string())?; keyring.set_password(uri).map_err(|e| e.to_string())?;
client.set_zapper(nwc).await; client.set_zapper(nwc).await;
@ -461,29 +463,25 @@ pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, Stri
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn load_wallet(state: State<'_, Nostr>) -> Result<String, String> { pub async fn load_wallet(state: State<'_, Nostr>) -> Result<(), String> {
let client = &state.client; let client = &state.client;
let keyring =
Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?;
match keyring.get_password() { if client.zapper().await.is_err() {
Ok(val) => { let keyring =
let uri = NostrWalletConnectURI::from_str(&val).unwrap(); Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?;
let nwc = NWC::new(uri);
// Get current balance match keyring.get_password() {
let balance = nwc.get_balance().await; Ok(val) => {
let uri = NostrWalletConnectURI::from_str(&val).unwrap();
let nwc = NWC::new(uri);
// Update zapper client.set_zapper(nwc).await;
client.set_zapper(nwc).await;
match balance {
Ok(val) => Ok(val.to_string()),
Err(_) => Err("Get balance failed.".into()),
} }
Err(_) => return Err("Wallet not found.".into()),
} }
Err(_) => Err("NWC not found.".into()),
} }
Ok(())
} }
#[tauri::command] #[tauri::command]
@ -505,52 +503,40 @@ pub async fn remove_wallet(state: State<'_, Nostr>) -> Result<(), String> {
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn zap_profile( pub async fn zap_profile(
id: &str, id: String,
amount: &str, amount: String,
message: &str, message: Option<String>,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<bool, String> { ) -> Result<(), String> {
let client = &state.client; let client = &state.client;
let public_key: PublicKey = PublicKey::parse(id).map_err(|e| e.to_string())?; let public_key: PublicKey = PublicKey::parse(id).map_err(|e| e.to_string())?;
let details = ZapDetails::new(ZapType::Private).message(message);
let num = amount.parse::<u64>().map_err(|e| e.to_string())?; let num = amount.parse::<u64>().map_err(|e| e.to_string())?;
let details = message.map(|m| ZapDetails::new(ZapType::Public).message(m));
if client.zap(public_key, num, Some(details)).await.is_ok() { match client.zap(public_key, num, details).await {
Ok(true) Ok(()) => Ok(()),
} else { Err(e) => Err(e.to_string()),
Err("Zap profile failed".into())
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn zap_event( pub async fn zap_event(
id: &str, id: String,
amount: &str, amount: String,
message: &str, message: Option<String>,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<bool, String> { ) -> Result<(), String> {
let client = &state.client; let client = &state.client;
let event_id = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::EventId(id) => id,
Nip19::Event(event) => event.event_id,
_ => return Err("Event ID is invalid.".into()),
},
Err(_) => match EventId::from_hex(id) {
Ok(val) => val,
Err(_) => return Err("Event ID is invalid.".into()),
},
};
let details = ZapDetails::new(ZapType::Private).message(message); let event_id = EventId::from_str(&id).map_err(|e| e.to_string())?;
let num = amount.parse::<u64>().map_err(|e| e.to_string())?; let num = amount.parse::<u64>().map_err(|e| e.to_string())?;
let details = message.map(|m| ZapDetails::new(ZapType::Public).message(m));
if client.zap(event_id, num, Some(details)).await.is_ok() { match client.zap(event_id, num, details).await {
Ok(true) Ok(()) => Ok(()),
} else { Err(e) => Err(e.to_string()),
Err("Zap event failed".into())
} }
} }

View File

@ -90,8 +90,7 @@ struct Subscription {
struct NewSettings(Settings); struct NewSettings(Settings);
pub const DEFAULT_DIFFICULTY: u8 = 21; pub const DEFAULT_DIFFICULTY: u8 = 21;
pub const FETCH_LIMIT: usize = 100; pub const FETCH_LIMIT: usize = 50;
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
pub const NOTIFICATION_SUB_ID: &str = "lume_notification"; pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
fn main() { fn main() {
@ -232,10 +231,10 @@ fn main() {
// Config // Config
let opts = Options::new() let opts = Options::new()
.gossip(true) .gossip(true)
.max_avg_latency(Duration::from_millis(500)) .max_avg_latency(Duration::from_millis(800))
.automatic_authentication(false) .automatic_authentication(false)
.connection_timeout(Some(Duration::from_secs(20))) .connection_timeout(Some(Duration::from_secs(20)))
.send_timeout(Some(Duration::from_secs(10))) .send_timeout(Some(Duration::from_secs(20)))
.timeout(Duration::from_secs(20)); .timeout(Duration::from_secs(20));
// Setup nostr client // Setup nostr client
@ -479,6 +478,17 @@ fn main() {
println!("Error: {}", e); println!("Error: {}", e);
} }
// Workaround for https://github.com/rust-nostr/nostr/issues/509
// TODO: remove
let _ = client
.fetch_events(
vec![Filter::new()
.kind(Kind::TextNote)
.limit(0)],
Some(Duration::from_secs(5)),
)
.await;
if allow_notification { if allow_notification {
if let Err(e) = &handle_clone if let Err(e) = &handle_clone
.notification() .notification()

View File

@ -224,7 +224,7 @@ async setWallet(uri: string) : Promise<Result<boolean, string>> {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async loadWallet() : Promise<Result<string, string>> { async loadWallet() : Promise<Result<null, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("load_wallet") }; return { status: "ok", data: await TAURI_INVOKE("load_wallet") };
} catch (e) { } catch (e) {
@ -240,7 +240,7 @@ async removeWallet() : Promise<Result<null, string>> {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async zapProfile(id: string, amount: string, message: string) : Promise<Result<boolean, string>> { async zapProfile(id: string, amount: string, message: string | null) : Promise<Result<null, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("zap_profile", { id, amount, message }) }; return { status: "ok", data: await TAURI_INVOKE("zap_profile", { id, amount, message }) };
} catch (e) { } catch (e) {
@ -248,7 +248,7 @@ async zapProfile(id: string, amount: string, message: string) : Promise<Result<b
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async zapEvent(id: string, amount: string, message: string) : Promise<Result<boolean, string>> { async zapEvent(id: string, amount: string, message: string | null) : Promise<Result<null, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("zap_event", { id, amount, message }) }; return { status: "ok", data: await TAURI_INVOKE("zap_event", { id, amount, message }) };
} catch (e) { } catch (e) {

View File

@ -25,7 +25,6 @@ import { Route as SettingsIdWalletImport } from './routes/settings.$id/wallet'
import { Route as SettingsIdRelayImport } from './routes/settings.$id/relay' import { Route as SettingsIdRelayImport } from './routes/settings.$id/relay'
import { Route as SettingsIdProfileImport } from './routes/settings.$id/profile' import { Route as SettingsIdProfileImport } from './routes/settings.$id/profile'
import { Route as SettingsIdGeneralImport } from './routes/settings.$id/general' import { Route as SettingsIdGeneralImport } from './routes/settings.$id/general'
import { Route as SettingsIdBitcoinConnectImport } from './routes/settings.$id/bitcoin-connect'
import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global' import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global'
import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed' import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed'
import { Route as ColumnsLayoutStoriesIdImport } from './routes/columns/_layout/stories.$id' import { Route as ColumnsLayoutStoriesIdImport } from './routes/columns/_layout/stories.$id'
@ -215,13 +214,6 @@ const SettingsIdGeneralRoute = SettingsIdGeneralImport.update({
import('./routes/settings.$id/general.lazy').then((d) => d.Route), import('./routes/settings.$id/general.lazy').then((d) => d.Route),
) )
const SettingsIdBitcoinConnectRoute = SettingsIdBitcoinConnectImport.update({
path: '/bitcoin-connect',
getParentRoute: () => SettingsIdLazyRoute,
} as any).lazy(() =>
import('./routes/settings.$id/bitcoin-connect.lazy').then((d) => d.Route),
)
const ColumnsLayoutGlobalRoute = ColumnsLayoutGlobalImport.update({ const ColumnsLayoutGlobalRoute = ColumnsLayoutGlobalImport.update({
path: '/global', path: '/global',
getParentRoute: () => ColumnsLayoutRoute, getParentRoute: () => ColumnsLayoutRoute,
@ -436,13 +428,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ColumnsLayoutGlobalImport preLoaderRoute: typeof ColumnsLayoutGlobalImport
parentRoute: typeof ColumnsLayoutImport parentRoute: typeof ColumnsLayoutImport
} }
'/settings/$id/bitcoin-connect': {
id: '/settings/$id/bitcoin-connect'
path: '/bitcoin-connect'
fullPath: '/settings/$id/bitcoin-connect'
preLoaderRoute: typeof SettingsIdBitcoinConnectImport
parentRoute: typeof SettingsIdLazyImport
}
'/settings/$id/general': { '/settings/$id/general': {
id: '/settings/$id/general' id: '/settings/$id/general'
path: '/general' path: '/general'
@ -653,7 +638,6 @@ const ColumnsRouteWithChildren =
ColumnsRoute._addFileChildren(ColumnsRouteChildren) ColumnsRoute._addFileChildren(ColumnsRouteChildren)
interface SettingsIdLazyRouteChildren { interface SettingsIdLazyRouteChildren {
SettingsIdBitcoinConnectRoute: typeof SettingsIdBitcoinConnectRoute
SettingsIdGeneralRoute: typeof SettingsIdGeneralRoute SettingsIdGeneralRoute: typeof SettingsIdGeneralRoute
SettingsIdProfileRoute: typeof SettingsIdProfileRoute SettingsIdProfileRoute: typeof SettingsIdProfileRoute
SettingsIdRelayRoute: typeof SettingsIdRelayRoute SettingsIdRelayRoute: typeof SettingsIdRelayRoute
@ -661,7 +645,6 @@ interface SettingsIdLazyRouteChildren {
} }
const SettingsIdLazyRouteChildren: SettingsIdLazyRouteChildren = { const SettingsIdLazyRouteChildren: SettingsIdLazyRouteChildren = {
SettingsIdBitcoinConnectRoute: SettingsIdBitcoinConnectRoute,
SettingsIdGeneralRoute: SettingsIdGeneralRoute, SettingsIdGeneralRoute: SettingsIdGeneralRoute,
SettingsIdProfileRoute: SettingsIdProfileRoute, SettingsIdProfileRoute: SettingsIdProfileRoute,
SettingsIdRelayRoute: SettingsIdRelayRoute, SettingsIdRelayRoute: SettingsIdRelayRoute,
@ -690,7 +673,6 @@ export interface FileRoutesByFullPath {
'/new-post': typeof NewPostIndexRoute '/new-post': typeof NewPostIndexRoute
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
'/columns/global': typeof ColumnsLayoutGlobalRoute '/columns/global': typeof ColumnsLayoutGlobalRoute
'/settings/$id/bitcoin-connect': typeof SettingsIdBitcoinConnectRoute
'/settings/$id/general': typeof SettingsIdGeneralRoute '/settings/$id/general': typeof SettingsIdGeneralRoute
'/settings/$id/profile': typeof SettingsIdProfileRoute '/settings/$id/profile': typeof SettingsIdProfileRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute '/settings/$id/relay': typeof SettingsIdRelayRoute
@ -728,7 +710,6 @@ export interface FileRoutesByTo {
'/new-post': typeof NewPostIndexRoute '/new-post': typeof NewPostIndexRoute
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
'/columns/global': typeof ColumnsLayoutGlobalRoute '/columns/global': typeof ColumnsLayoutGlobalRoute
'/settings/$id/bitcoin-connect': typeof SettingsIdBitcoinConnectRoute
'/settings/$id/general': typeof SettingsIdGeneralRoute '/settings/$id/general': typeof SettingsIdGeneralRoute
'/settings/$id/profile': typeof SettingsIdProfileRoute '/settings/$id/profile': typeof SettingsIdProfileRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute '/settings/$id/relay': typeof SettingsIdRelayRoute
@ -769,7 +750,6 @@ export interface FileRoutesById {
'/new-post/': typeof NewPostIndexRoute '/new-post/': typeof NewPostIndexRoute
'/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute '/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
'/settings/$id/bitcoin-connect': typeof SettingsIdBitcoinConnectRoute
'/settings/$id/general': typeof SettingsIdGeneralRoute '/settings/$id/general': typeof SettingsIdGeneralRoute
'/settings/$id/profile': typeof SettingsIdProfileRoute '/settings/$id/profile': typeof SettingsIdProfileRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute '/settings/$id/relay': typeof SettingsIdRelayRoute
@ -810,7 +790,6 @@ export interface FileRouteTypes {
| '/new-post' | '/new-post'
| '/columns/create-newsfeed' | '/columns/create-newsfeed'
| '/columns/global' | '/columns/global'
| '/settings/$id/bitcoin-connect'
| '/settings/$id/general' | '/settings/$id/general'
| '/settings/$id/profile' | '/settings/$id/profile'
| '/settings/$id/relay' | '/settings/$id/relay'
@ -847,7 +826,6 @@ export interface FileRouteTypes {
| '/new-post' | '/new-post'
| '/columns/create-newsfeed' | '/columns/create-newsfeed'
| '/columns/global' | '/columns/global'
| '/settings/$id/bitcoin-connect'
| '/settings/$id/general' | '/settings/$id/general'
| '/settings/$id/profile' | '/settings/$id/profile'
| '/settings/$id/relay' | '/settings/$id/relay'
@ -886,7 +864,6 @@ export interface FileRouteTypes {
| '/new-post/' | '/new-post/'
| '/columns/_layout/create-newsfeed' | '/columns/_layout/create-newsfeed'
| '/columns/_layout/global' | '/columns/_layout/global'
| '/settings/$id/bitcoin-connect'
| '/settings/$id/general' | '/settings/$id/general'
| '/settings/$id/profile' | '/settings/$id/profile'
| '/settings/$id/relay' | '/settings/$id/relay'
@ -1035,7 +1012,6 @@ export const routeTree = rootRoute
"/settings/$id": { "/settings/$id": {
"filePath": "settings.$id.lazy.tsx", "filePath": "settings.$id.lazy.tsx",
"children": [ "children": [
"/settings/$id/bitcoin-connect",
"/settings/$id/general", "/settings/$id/general",
"/settings/$id/profile", "/settings/$id/profile",
"/settings/$id/relay", "/settings/$id/relay",
@ -1061,10 +1037,6 @@ export const routeTree = rootRoute
"filePath": "columns/_layout/global.tsx", "filePath": "columns/_layout/global.tsx",
"parent": "/columns/_layout" "parent": "/columns/_layout"
}, },
"/settings/$id/bitcoin-connect": {
"filePath": "settings.$id/bitcoin-connect.tsx",
"parent": "/settings/$id"
},
"/settings/$id/general": { "/settings/$id/general": {
"filePath": "settings.$id/general.tsx", "filePath": "settings.$id/general.tsx",
"parent": "/settings/$id" "parent": "/settings/$id"

View File

@ -4,10 +4,10 @@ import { PublishIcon } from "@/components";
import { User } from "@/components/user"; import { User } from "@/components/user";
import { LumeWindow } from "@/system"; import { LumeWindow } from "@/system";
import { MagnifyingGlass, Plus } from "@phosphor-icons/react"; import { MagnifyingGlass, Plus } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router"; import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { memo, useCallback, useEffect, useState } from "react"; import { memo, useCallback, useEffect, useState } from "react";
@ -115,11 +115,22 @@ const NegentropyBadge = memo(function NegentropyBadge() {
); );
}); });
const Account = memo(function Account({ pubkey }: { pubkey: string }) { function Account({ pubkey }: { pubkey: string }) {
const navigate = Route.useNavigate(); const navigate = Route.useNavigate();
const context = Route.useRouteContext(); const context = Route.useRouteContext();
const [isActive, setIsActive] = useState(false); const { data: isActive } = useQuery({
queryKey: ["signer", pubkey],
queryFn: async () => {
const res = await commands.hasSigner(pubkey);
if (res.status === "ok") {
return res.data;
} else {
return false;
}
},
});
const showContextMenu = useCallback( const showContextMenu = useCallback(
async (e: React.MouseEvent) => { async (e: React.MouseEvent) => {
@ -165,26 +176,6 @@ const Account = memo(function Account({ pubkey }: { pubkey: string }) {
[pubkey], [pubkey],
); );
useEffect(() => {
(async () => {
const res = await commands.hasSigner(pubkey);
if (res.status === "ok" && res.data) {
setIsActive(true);
}
})();
}, [pubkey]);
useEffect(() => {
const unlisten = getCurrentWindow().listen("signer-updated", async () => {
setIsActive((prev) => !prev);
});
return () => {
unlisten.then((f) => f());
};
}, []);
return ( return (
<button <button
type="button" type="button"
@ -197,8 +188,8 @@ const Account = memo(function Account({ pubkey }: { pubkey: string }) {
</User.Root> </User.Root>
</User.Provider> </User.Provider>
{isActive ? ( {isActive ? (
<div className="h-px w-full absolute bottom-0 left-0 bg-blue-500 rounded-full" /> <div className="h-px w-full absolute bottom-0 left-0 bg-green-500 rounded-full" />
) : null} ) : null}
</button> </button>
); );
}); }

View File

@ -17,10 +17,13 @@ function Screen() {
<div <div
data-tauri-drag-region data-tauri-drag-region
className={cn( className={cn(
"w-[250px] shrink-0 flex flex-col gap-1 border-r border-black/10 dark:border-white/10 p-2", "w-[200px] shrink-0 flex flex-col gap-1 border-r border-black/10 dark:border-white/10 p-2",
platform === "macos" ? "pt-11" : "", platform === "macos" ? "pt-11" : "",
)} )}
> >
<div className="h-8 px-1.5">
<h1 className="text-lg font-semibold">Settings</h1>
</div>
<Link to="/settings/$id/general" params={{ id }}> <Link to="/settings/$id/general" params={{ id }}>
{({ isActive }) => { {({ isActive }) => {
return ( return (

View File

@ -1,37 +0,0 @@
import { commands } from "@/commands.gen";
import { Button } from "@getalby/bitcoin-connect-react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
export const Route = createLazyFileRoute("/settings/$id/bitcoin-connect")({
component: Screen,
});
function Screen() {
const setNwcUri = async (uri: string) => {
const res = await commands.setWallet(uri);
if (res.status === "ok") {
await getCurrentWebviewWindow().close();
} else {
throw new Error(res.error);
}
};
return (
<div className="flex items-center justify-center size-full">
<div className="flex flex-col items-center justify-center gap-3 text-center">
<div>
<p className="text-sm text-black/70 dark:text-white/70">
Click to the button below to connect with your Bitcoin wallet.
</p>
</div>
<Button
onConnected={(provider) =>
setNwcUri(provider.client.nostrWalletConnectUrl)
}
/>
</div>
</div>
);
}

View File

@ -1,12 +0,0 @@
import { init } from '@getalby/bitcoin-connect-react'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/$id/bitcoin-connect')({
beforeLoad: () => {
init({
appName: 'Lume',
filters: ['nwc'],
showBalance: true,
})
},
})

View File

@ -2,8 +2,8 @@ import { commands } from "@/commands.gen";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/settings/$id/relay")({ export const Route = createFileRoute("/settings/$id/relay")({
beforeLoad: async () => { beforeLoad: async ({ params }) => {
const res = await commands.getRelays(); const res = await commands.getRelays(params.id);
if (res.status === "ok") { if (res.status === "ok") {
return { relayList: res.data }; return { relayList: res.data };

View File

@ -1,20 +1,30 @@
import { commands } from "@/commands.gen"; import { commands } from "@/commands.gen";
import { createLazyFileRoute, redirect } from "@tanstack/react-router"; import { Button } from "@getalby/bitcoin-connect-react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createLazyFileRoute("/settings/$id/wallet")({ export const Route = createLazyFileRoute("/settings/$id/wallet")({
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const { id } = Route.useParams(); const [_isConnect, setIsConnect] = useState(false);
const { balance } = Route.useRouteContext();
const disconnect = async () => { const setWallet = async (uri: string) => {
const res = await commands.setWallet(uri);
if (res.status === "ok") {
setIsConnect((prev) => !prev);
} else {
throw new Error(res.error);
}
};
const removeWallet = async () => {
const res = await commands.removeWallet(); const res = await commands.removeWallet();
if (res.status === "ok") { if (res.status === "ok") {
window.localStorage.removeItem("bc:config"); window.localStorage.removeItem("bc:config");
return redirect({ to: "/settings/$id/bitcoin-connect", params: { id } });
} else { } else {
throw new Error(res.error); throw new Error(res.error);
} }
@ -22,32 +32,17 @@ function Screen() {
return ( return (
<div className="w-full px-3 pb-3"> <div className="w-full px-3 pb-3">
<div className="flex flex-col w-full gap-3"> <div className="flex flex-col w-full gap-2">
<div className="flex flex-col w-full px-3 bg-black/5 dark:bg-white/5 rounded-xl"> <h2 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
<div className="flex items-center justify-between w-full gap-4 py-3"> Wallet
<div className="flex-1"> </h2>
<h3 className="font-medium">Connection</h3> <div className="w-full h-44 flex items-center justify-center bg-black/5 dark:bg-white/5 rounded-xl">
</div> <Button
<div className="flex justify-end w-36 shrink-0"> onConnected={(provider) =>
<button setWallet(provider.client.nostrWalletConnectUrl)
type="button" }
onClick={() => disconnect()} onDisconnected={() => removeWallet()}
className="h-8 w-max px-2.5 text-sm rounded-lg inline-flex items-center justify-center bg-black/10 dark:bg-white/10 hover:bg-black/20 dark:hover:bg-white/20" />
>
Disconnect
</button>
</div>
</div>
</div>
<div className="flex flex-col w-full px-3 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-between w-full gap-4 py-3">
<div className="flex-1">
<h3 className="font-medium">Current Balance</h3>
</div>
<div className="flex justify-end w-36 shrink-0">
{balance.bitcoinFormatted}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,21 +1,12 @@
import { commands } from "@/commands.gen"; import { init } from "@getalby/bitcoin-connect-react";
import { getBitcoinDisplayValues } from "@/commons"; import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/settings/$id/wallet")({ export const Route = createFileRoute("/settings/$id/wallet")({
beforeLoad: async ({ params }) => { beforeLoad: async () => {
const query = await commands.loadWallet(); init({
appName: "Lume",
if (query.status === "ok") { filters: ["nwc"],
const wallet = Number.parseInt(query.data); showBalance: true,
const balance = getBitcoinDisplayValues(wallet); });
return { balance };
} else {
throw redirect({
to: "/settings/$id/bitcoin-connect",
params: { id: params.id },
});
}
}, },
}); });

View File

@ -1,8 +1,14 @@
import { User } from "@/components/user"; import { commands } from "@/commands.gen";
import { displayNpub } from "@/commons";
import { User } from "@/components";
import { LumeWindow } from "@/system";
import type { Metadata } from "@/types";
import { CaretDown } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { type Window, getCurrentWindow } from "@tauri-apps/api/window";
import { message } from "@tauri-apps/plugin-dialog"; import { message } from "@tauri-apps/plugin-dialog";
import { useState, useTransition } from "react"; import { useCallback, useEffect, useState, useTransition } from "react";
import CurrencyInput from "react-currency-input-field"; import CurrencyInput from "react-currency-input-field";
const DEFAULT_VALUES = [21, 50, 100, 200]; const DEFAULT_VALUES = [21, 50, 100, 200];
@ -12,38 +18,102 @@ export const Route = createLazyFileRoute("/zap/$id")({
}); });
function Screen() { function Screen() {
const { event } = Route.useRouteContext(); const { accounts, event } = Route.useRouteContext();
const [currentUser, setCurrentUser] = useState<string>(null);
const [popup, setPopup] = useState<Window>(null);
const [amount, setAmount] = useState(21); const [amount, setAmount] = useState(21);
const [content, setContent] = useState(""); const [content, setContent] = useState<string>("");
const [isCompleted, setIsCompleted] = useState(false); const [isCompleted, setIsCompleted] = useState(false);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const submit = () => { const showContextMenu = useCallback(async (e: React.MouseEvent) => {
startTransition(async () => { e.preventDefault();
try {
const val = await event.zap(amount, content);
if (val) { const list = [];
setIsCompleted(true);
// close current window for (const account of accounts) {
await getCurrentWebviewWindow().close(); const res = await commands.getProfile(account);
} let name = "unknown";
} catch (e) {
await message(String(e), { if (res.status === "ok") {
title: "Zap", const profile: Metadata = JSON.parse(res.data);
kind: "error", name = profile.display_name ?? profile.name;
}); }
list.push(
MenuItem.new({
text: `Zap as ${name} (${displayNpub(account, 16)})`,
action: async () => setCurrentUser(account),
}),
);
}
const items = await Promise.all(list);
const menu = await Menu.new({ items });
await menu.popup().catch((e) => console.error(e));
}, []);
const zap = () => {
startTransition(async () => {
const res = await commands.zapEvent(event.id, amount.toString(), content);
if (res.status === "ok") {
setIsCompleted(true);
// close current window
await getCurrentWindow().close();
} else {
await message(res.error, { kind: "error" });
return; return;
} }
}); });
}; };
const submit = async () => {
if (currentUser) {
const signer = await commands.hasSigner(currentUser);
if (signer.status === "ok") {
if (!signer.data) {
const newPopup = await LumeWindow.openPopup(
`/set-signer/${currentUser}`,
undefined,
false,
);
setPopup(newPopup);
return;
}
zap();
}
}
};
useEffect(() => {
if (!popup) return;
const unlisten = popup.listen("signer-updated", () => {
zap();
});
return () => {
unlisten.then((f) => f());
};
}, [popup]);
useEffect(() => {
if (accounts?.length) {
setCurrentUser(accounts[0]);
}
}, [accounts]);
return ( return (
<div data-tauri-drag-region className="flex flex-col pb-5 size-full"> <div data-tauri-drag-region className="flex flex-col pb-5 size-full">
<div <div
data-tauri-drag-region data-tauri-drag-region
className="flex items-center justify-center h-24 gap-2 shrink-0" className="flex items-center justify-center h-32 gap-2 shrink-0"
> >
<p className="text-sm">Send zap to </p> <p className="text-sm">Send zap to </p>
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>
@ -95,15 +165,34 @@ function Screen() {
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
placeholder="Enter message (optional)" placeholder="Enter message (optional)"
className="h-11 w-full resize-none rounded-xl border-transparent bg-black/5 px-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/5" className="h-10 w-full resize-none rounded-lg border-transparent bg-black/5 px-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/5"
/> />
<button <div className="inline-flex items-center gap-3">
type="button" <button
onClick={() => submit()} type="button"
className="inline-flex items-center justify-center w-full h-10 font-medium rounded-xl bg-neutral-950 text-neutral-50 hover:bg-neutral-900 dark:bg-white/20 dark:hover:bg-white/30" onClick={() => submit()}
> className="inline-flex items-center justify-center w-full h-9 text-sm font-semibold rounded-lg bg-blue-500 text-white hover:bg-blue-600 dark:hover:bg-blue-400"
{isCompleted ? "Zapped" : isPending ? "Processing..." : "Zap"} >
</button> {isCompleted ? "Zapped" : isPending ? "Processing..." : "Zap"}
</button>
{currentUser ? (
<button
type="button"
onClick={(e) => showContextMenu(e)}
className="inline-flex items-center gap-1.5"
>
<User.Provider pubkey={currentUser}>
<User.Root>
<User.Avatar className="size-6 rounded-full" />
</User.Root>
</User.Provider>
<CaretDown
className="mt-px size-3 text-neutral-500"
weight="bold"
/>
</button>
) : null}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,6 +5,7 @@ import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/zap/$id")({ export const Route = createFileRoute("/zap/$id")({
beforeLoad: async ({ params }) => { beforeLoad: async ({ params }) => {
const accounts = await commands.getAccounts();
const res = await commands.getEvent(params.id); const res = await commands.getEvent(params.id);
if (res.status === "ok") { if (res.status === "ok") {
@ -15,7 +16,7 @@ export const Route = createFileRoute("/zap/$id")({
raw.meta = data.parsed; raw.meta = data.parsed;
} }
return { event: new LumeEvent(raw) }; return { accounts, event: new LumeEvent(raw) };
} else { } else {
throw new Error(res.error); throw new Error(res.error);
} }

View File

@ -145,7 +145,7 @@ export const LumeWindow = {
closable: true, closable: true,
}); });
} else { } else {
await LumeWindow.openSettings(account, "bitcoin-connect"); await LumeWindow.openSettings(account, "wallet");
} }
}, },
openSettings: async (account: string, path?: string) => { openSettings: async (account: string, path?: string) => {
@ -155,7 +155,7 @@ export const LumeWindow = {
? `/settings/${account}/${path}` ? `/settings/${account}/${path}`
: `/settings/${account}/general`, : `/settings/${account}/general`,
title: "Settings", title: "Settings",
width: 800, width: 700,
height: 500, height: 500,
maximizable: false, maximizable: false,
minimizable: false, minimizable: false,