diff --git a/apps/desktop2/src/app.css b/apps/desktop2/src/app.css index 3e302a80..4511c0a5 100644 --- a/apps/desktop2/src/app.css +++ b/apps/desktop2/src/app.css @@ -35,6 +35,12 @@ media-controller { } .shadow-toolbar { - box-shadow: 0 0 #0000, 0 0 #0000, 0 8px 24px 0 rgba(0, 0, 0, .2), 0 2px 8px 0 rgba(0, 0, 0, .08), inset 0 0 0 1px rgba(0, 0, 0, .2), inset 0 0 0 2px hsla(0, 0%, 100%, .14) + box-shadow: + 0 0 #0000, + 0 0 #0000, + 0 8px 24px 0 rgba(0, 0, 0, 0.2), + 0 2px 8px 0 rgba(0, 0, 0, 0.08), + inset 0 0 0 1px rgba(0, 0, 0, 0.2), + inset 0 0 0 2px hsla(0, 0%, 100%, 0.14); } } diff --git a/apps/desktop2/src/routes/$account/home/-components/repost.tsx b/apps/desktop2/src/components/repost.tsx similarity index 97% rename from apps/desktop2/src/routes/$account/home/-components/repost.tsx rename to apps/desktop2/src/components/repost.tsx index ea0d665e..de7e9746 100644 --- a/apps/desktop2/src/routes/$account/home/-components/repost.tsx +++ b/apps/desktop2/src/components/repost.tsx @@ -77,7 +77,7 @@ export function RepostNote({ > -
+
@@ -93,7 +93,7 @@ export function RepostNote({
-
+
diff --git a/apps/desktop2/src/routes/$account/home/-components/text.tsx b/apps/desktop2/src/components/text.tsx similarity index 100% rename from apps/desktop2/src/routes/$account/home/-components/text.tsx rename to apps/desktop2/src/components/text.tsx diff --git a/apps/desktop2/src/routes/$account/home/local.lazy.tsx b/apps/desktop2/src/routes/$account/home/local.lazy.tsx index 7cfc47d1..02b099f3 100644 --- a/apps/desktop2/src/routes/$account/home/local.lazy.tsx +++ b/apps/desktop2/src/routes/$account/home/local.lazy.tsx @@ -1,18 +1,13 @@ import { useArk } from "@lume/ark"; -import { - ArrowRightCircleIcon, - ArrowRightIcon, - LoaderIcon, - SearchIcon, -} from "@lume/icons"; +import { ArrowRightCircleIcon, ArrowRightIcon, LoaderIcon } from "@lume/icons"; import { Event, Kind } from "@lume/types"; import { EmptyFeed } from "@lume/ui"; import { FETCH_LIMIT } from "@lume/utils"; import { useInfiniteQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; import { Virtualizer } from "virtua"; -import { TextNote } from "./-components/text"; -import { RepostNote } from "./-components/repost"; +import { TextNote } from "@/components/text"; +import { RepostNote } from "@/components/repost"; export const Route = createLazyFileRoute("/$account/home/local")({ component: LocalTimeline, @@ -64,13 +59,6 @@ function LocalTimeline() { ) : !data.length ? ( ) : ( diff --git a/apps/desktop2/src/routes/users/-components/eventList.tsx b/apps/desktop2/src/routes/users/-components/eventList.tsx index f3012179..eabaada2 100644 --- a/apps/desktop2/src/routes/users/-components/eventList.tsx +++ b/apps/desktop2/src/routes/users/-components/eventList.tsx @@ -1,5 +1,5 @@ -import { RepostNote } from "@/routes/$account/home/-components/repost"; -import { TextNote } from "@/routes/$account/home/-components/text"; +import { TextNote } from "@/components/text"; +import { RepostNote } from "@/components/repost"; import { useArk } from "@lume/ark"; import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons"; import { Event, Kind } from "@lume/types"; diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index b6c592ed..e34c6cb9 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -394,6 +394,42 @@ export class Ark { } } + public async get_nwc_status() { + try { + const cmd: boolean = await invoke("get_nwc_status"); + return cmd; + } catch { + return false; + } + } + + public async set_nwc(uri: string) { + try { + const cmd: boolean = await invoke("set_nwc", { uri }); + 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 }); + return cmd; + } catch { + return false; + } + } + + public async zap_event(id: string, amount: number, message: string) { + try { + const cmd: boolean = await invoke("zap_event", { id, amount, message }); + return cmd; + } catch { + return false; + } + } + public async upload(filePath?: string) { try { const allowExts = [ diff --git a/packages/ui/src/note/buttons/downvote.tsx b/packages/ui/src/note/buttons/downvote.tsx index 37d1e3a4..439790ea 100644 --- a/packages/ui/src/note/buttons/downvote.tsx +++ b/packages/ui/src/note/buttons/downvote.tsx @@ -3,11 +3,14 @@ import { useState } from "react"; import { useNoteContext } from "../provider"; import { useArk } from "@lume/ark"; import { cn } from "@lume/utils"; +import * as Tooltip from "@radix-ui/react-tooltip"; +import { useTranslation } from "react-i18next"; export function NoteDownvote() { const ark = useArk(); const event = useNoteContext(); + const [t] = useTranslation(); const [reaction, setReaction] = useState<"-" | null>(null); const [loading, setLoading] = useState(false); @@ -23,22 +26,34 @@ export function NoteDownvote() { }; return ( - + + + + + + + + {t("note.buttons.downvote")} + + + + + ); } diff --git a/packages/ui/src/note/buttons/upvote.tsx b/packages/ui/src/note/buttons/upvote.tsx index 9e760a93..2aaf6184 100644 --- a/packages/ui/src/note/buttons/upvote.tsx +++ b/packages/ui/src/note/buttons/upvote.tsx @@ -3,11 +3,14 @@ import { useState } from "react"; import { useNoteContext } from "../provider"; import { useArk } from "@lume/ark"; import { cn } from "@lume/utils"; +import * as Tooltip from "@radix-ui/react-tooltip"; +import { useTranslation } from "react-i18next"; export function NoteUpvote() { const ark = useArk(); const event = useNoteContext(); + const [t] = useTranslation(); const [reaction, setReaction] = useState<"+" | null>(null); const [loading, setLoading] = useState(false); @@ -23,22 +26,34 @@ export function NoteUpvote() { }; return ( - + + + + + + + + {t("note.buttons.upvote")} + + + + + ); } diff --git a/packages/ui/src/note/menu.tsx b/packages/ui/src/note/menu.tsx index 6b5febc3..9680b2ea 100644 --- a/packages/ui/src/note/menu.tsx +++ b/packages/ui/src/note/menu.tsx @@ -95,6 +95,7 @@ export function NoteMenu() { {t("note.menu.copyRaw")} + diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 39e28285..6dae87f6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -106,6 +106,10 @@ fn main() { nostr::metadata::get_interest, nostr::metadata::set_settings, nostr::metadata::get_settings, + nostr::metadata::get_nwc_status, + nostr::metadata::set_nwc, + nostr::metadata::zap_profile, + nostr::metadata::zap_event, nostr::event::get_event, nostr::event::get_events_from, nostr::event::get_local_events, diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index 69eff7d9..1a528986 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -3,6 +3,7 @@ use keyring::Entry; use nostr_sdk::prelude::*; use std::io::{BufReader, Read}; use std::iter; +use std::time::Duration; use std::{fs::File, io::Write, str::FromStr}; use tauri::{Manager, State}; @@ -139,11 +140,34 @@ pub async fn load_selected_account( // Build nostr signer let secret_key = SecretKey::from_bech32(nsec_key).expect("Get secret key failed"); let keys = Keys::new(secret_key); + let public_key = keys.public_key(); let signer = NostrSigner::Keys(keys); // Update signer client.set_signer(Some(signer)).await; + // Get user's relay list + let filter = Filter::new() + .author(public_key) + .kind(Kind::RelayList) + .limit(1); + let query = client + .get_events_of(vec![filter], Some(Duration::from_secs(10))) + .await; + + // Connect user's relay list + if let Ok(events) = query { + if let Some(event) = events.first() { + let list = nip65::extract_relay_list(&event); + for item in list.into_iter() { + client + .connect_relay(item.0.to_string()) + .await + .unwrap_or_default(); + } + } + } + Ok(true) } else { Ok(false) diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index 808b1a05..3169530b 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -272,3 +272,93 @@ pub async fn get_settings(id: &str, state: State<'_, Nostr>) -> Result) -> Result { + let client = &state.client; + let zapper = client.zapper().await.is_ok(); + + Ok(zapper) +} + +#[tauri::command] +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 { + let _ = client.set_zapper(nwc); + Ok(true) + } else { + Ok(false) + } + } else { + Err("Set NWC failed".into()) + } +} + +#[tauri::command] +pub async fn zap_profile( + id: &str, + amount: u64, + message: &str, + state: State<'_, Nostr>, +) -> Result { + let client = &state.client; + let public_key: Option = match Nip19::from_bech32(id) { + Ok(val) => match val { + Nip19::Pubkey(pubkey) => Some(pubkey), + Nip19::Profile(profile) => Some(profile.public_key), + _ => None, + }, + Err(_) => match PublicKey::from_str(id) { + Ok(val) => Some(val), + Err(_) => None, + }, + }; + + if let Some(recipient) = public_key { + let details = ZapDetails::new(ZapType::Public).message(message); + + if let Ok(_) = client.zap(recipient, amount, Some(details)).await { + Ok(true) + } else { + Err("Zap profile failed".into()) + } + } else { + Err("Parse public key failed".into()) + } +} + +#[tauri::command] +pub async fn zap_event( + id: &str, + amount: u64, + message: &str, + state: State<'_, Nostr>, +) -> Result { + let client = &state.client; + let event_id: Option = match Nip19::from_bech32(id) { + Ok(val) => match val { + Nip19::EventId(id) => Some(id), + Nip19::Event(event) => Some(event.event_id), + _ => None, + }, + Err(_) => match EventId::from_hex(id) { + Ok(val) => Some(val), + Err(_) => None, + }, + }; + + if let Some(recipient) = event_id { + let details = ZapDetails::new(ZapType::Public).message(message); + + if let Ok(_) = client.zap(recipient, amount, Some(details)).await { + Ok(true) + } else { + Err("Zap event failed".into()) + } + } else { + Err("Parse public key failed".into()) + } +}