From 07ce253f5b0eed3713759af875ded2bb5e96f3b2 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:10:29 +0700 Subject: [PATCH] fix: child webview is not reposition after scroll --- apps/desktop2/src/components/column.tsx | 10 +- apps/desktop2/src/routes/$account.home.tsx | 8 +- apps/desktop2/src/routes/__root.tsx | 1 - apps/desktop2/src/routes/panel.$account.tsx | 4 + src-tauri/.rustfmt.toml | 1 - src-tauri/src/commands/fns.rs | 204 ++-- src-tauri/src/commands/tray.rs | 88 +- src-tauri/src/commands/window.rs | 381 ++++---- src-tauri/src/main.rs | 413 ++++---- src-tauri/src/nostr/event.rs | 995 ++++++++++---------- src-tauri/src/nostr/internal.rs | 120 +-- src-tauri/src/nostr/keys.rs | 665 ++++++------- src-tauri/src/nostr/metadata.rs | 815 ++++++++-------- src-tauri/src/nostr/relay.rs | 218 ++--- src-tauri/src/nostr/utils.rs | 394 ++++---- src-tauri/tauri.linux.conf.json | 1 - src-tauri/tauri.macos.conf.json | 6 - 17 files changed, 2163 insertions(+), 2161 deletions(-) delete mode 100644 src-tauri/.rustfmt.toml diff --git a/apps/desktop2/src/components/column.tsx b/apps/desktop2/src/components/column.tsx index a8a49bc2..13ae0704 100644 --- a/apps/desktop2/src/components/column.tsx +++ b/apps/desktop2/src/components/column.tsx @@ -3,7 +3,7 @@ import type { LumeColumn } from "@lume/types"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; -import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { getCurrentWindow } from "@tauri-apps/api/window"; import { memo, useCallback, useEffect, useRef, useState } from "react"; type WindowEvent = { @@ -106,7 +106,7 @@ function Header({ const [isChanged, setIsChanged] = useState(false); const saveNewTitle = async () => { - const mainWindow = getCurrentWebviewWindow(); + const mainWindow = getCurrentWindow(); await mainWindow.emit("columns", { type: "set_title", label, title }); // update search params @@ -135,7 +135,7 @@ function Header({ MenuItem.new({ text: "Move left", action: async () => { - await getCurrentWebviewWindow().emit("columns", { + await getCurrentWindow().emit("columns", { type: "move", label, direction: "left", @@ -145,7 +145,7 @@ function Header({ MenuItem.new({ text: "Move right", action: async () => { - await getCurrentWebviewWindow().emit("columns", { + await getCurrentWindow().emit("columns", { type: "move", label, direction: "right", @@ -156,7 +156,7 @@ function Header({ MenuItem.new({ text: "Close", action: async () => { - await getCurrentWebviewWindow().emit("columns", { + await getCurrentWindow().emit("columns", { type: "remove", label, }); diff --git a/apps/desktop2/src/routes/$account.home.tsx b/apps/desktop2/src/routes/$account.home.tsx index 394b3361..a4f0787a 100644 --- a/apps/desktop2/src/routes/$account.home.tsx +++ b/apps/desktop2/src/routes/$account.home.tsx @@ -30,11 +30,11 @@ function Screen() { }); const scrollPrev = useCallback(() => { - if (emblaApi) emblaApi.scrollPrev(true); + if (emblaApi) emblaApi.scrollPrev(); }, [emblaApi]); const scrollNext = useCallback(() => { - if (emblaApi) emblaApi.scrollNext(true); + if (emblaApi) emblaApi.scrollNext(); }, [emblaApi]); const emitScrollEvent = useCallback(() => { @@ -101,10 +101,10 @@ function Screen() { switch (event.code) { case "ArrowLeft": - if (emblaApi) emblaApi.scrollPrev(true); + if (emblaApi) emblaApi.scrollPrev(); break; case "ArrowRight": - if (emblaApi) emblaApi.scrollNext(true); + if (emblaApi) emblaApi.scrollNext(); break; default: break; diff --git a/apps/desktop2/src/routes/__root.tsx b/apps/desktop2/src/routes/__root.tsx index a3abdb7a..06232ce8 100644 --- a/apps/desktop2/src/routes/__root.tsx +++ b/apps/desktop2/src/routes/__root.tsx @@ -11,7 +11,6 @@ interface RouterContext { export const Route = createRootRouteWithContext()({ component: () => , pendingComponent: Pending, - wrapInSuspense: true, }); function Pending() { diff --git a/apps/desktop2/src/routes/panel.$account.tsx b/apps/desktop2/src/routes/panel.$account.tsx index 59433033..17d7676e 100644 --- a/apps/desktop2/src/routes/panel.$account.tsx +++ b/apps/desktop2/src/routes/panel.$account.tsx @@ -21,6 +21,9 @@ import { type ReactNode, useCallback, useEffect, useRef } from "react"; import { Virtualizer } from "virtua"; export const Route = createFileRoute("/panel/$account")({ + beforeLoad: async ({ context }) => { + console.log(context); + }, component: Screen, }); @@ -30,6 +33,7 @@ function Screen() { const { isLoading, data } = useQuery({ queryKey: ["notification", account], queryFn: async () => { + console.log(queryClient); const events = await NostrQuery.getNotifications(); return events; }, diff --git a/src-tauri/.rustfmt.toml b/src-tauri/.rustfmt.toml deleted file mode 100644 index 28781c75..00000000 --- a/src-tauri/.rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -tab_spaces=2 diff --git a/src-tauri/src/commands/fns.rs b/src-tauri/src/commands/fns.rs index 3999e1ea..9addf967 100644 --- a/src-tauri/src/commands/fns.rs +++ b/src-tauri/src/commands/fns.rs @@ -2,173 +2,173 @@ use std::ffi::CString; use tauri::{AppHandle, Emitter, Listener, Manager, WebviewWindow}; use tauri_nspanel::{ - block::ConcreteBlock, - cocoa::{ - appkit::{NSMainMenuWindowLevel, NSView, NSWindow, NSWindowCollectionBehavior}, - base::{id, nil}, - foundation::{NSPoint, NSRect}, - }, - objc::{class, msg_send, runtime::NO, sel, sel_impl}, - panel_delegate, ManagerExt, WebviewWindowExt, + block::ConcreteBlock, + cocoa::{ + appkit::{NSMainMenuWindowLevel, NSView, NSWindow, NSWindowCollectionBehavior}, + base::{id, nil}, + foundation::{NSPoint, NSRect}, + }, + objc::{class, msg_send, runtime::NO, sel, sel_impl}, + panel_delegate, ManagerExt, WebviewWindowExt, }; #[allow(non_upper_case_globals)] const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7; pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) { - let panel_delegate = panel_delegate!(SpotlightPanelDelegate { - window_did_resign_key - }); + let panel_delegate = panel_delegate!(SpotlightPanelDelegate { + window_did_resign_key + }); - let window = app_handle.get_webview_window("panel").unwrap(); + let window = app_handle.get_webview_window("panel").unwrap(); - let panel = window.to_panel().unwrap(); + let panel = window.to_panel().unwrap(); - let handle = app_handle.clone(); + let handle = app_handle.clone(); - panel_delegate.set_listener(Box::new(move |delegate_name: String| { - if delegate_name.as_str() == "window_did_resign_key" { - let _ = handle.emit("menubar_panel_did_resign_key", ()); - } - })); + panel_delegate.set_listener(Box::new(move |delegate_name: String| { + if delegate_name.as_str() == "window_did_resign_key" { + let _ = handle.emit("menubar_panel_did_resign_key", ()); + } + })); - panel.set_level(NSMainMenuWindowLevel + 1); + panel.set_level(NSMainMenuWindowLevel + 1); - panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel); + panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel); - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary, - ); + panel.set_collection_behaviour( + NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces + | NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary + | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary, + ); - panel.set_delegate(panel_delegate); + panel.set_delegate(panel_delegate); } pub fn setup_menubar_panel_listeners(app_handle: &AppHandle) { - fn hide_menubar_panel(app_handle: &tauri::AppHandle) { - if check_menubar_frontmost() { - return; + fn hide_menubar_panel(app_handle: &tauri::AppHandle) { + if check_menubar_frontmost() { + return; + } + + let panel = app_handle.get_webview_panel("panel").unwrap(); + + panel.order_out(None); } - let panel = app_handle.get_webview_panel("panel").unwrap(); + let handle = app_handle.clone(); - panel.order_out(None); - } + app_handle.listen_any("menubar_panel_did_resign_key", move |_| { + hide_menubar_panel(&handle); + }); - let handle = app_handle.clone(); + let handle = app_handle.clone(); - app_handle.listen_any("menubar_panel_did_resign_key", move |_| { - hide_menubar_panel(&handle); - }); + let callback = Box::new(move || { + hide_menubar_panel(&handle); + }); - let handle = app_handle.clone(); + register_workspace_listener( + "NSWorkspaceDidActivateApplicationNotification".into(), + callback.clone(), + ); - let callback = Box::new(move || { - hide_menubar_panel(&handle); - }); - - register_workspace_listener( - "NSWorkspaceDidActivateApplicationNotification".into(), - callback.clone(), - ); - - register_workspace_listener( - "NSWorkspaceActiveSpaceDidChangeNotification".into(), - callback, - ); + register_workspace_listener( + "NSWorkspaceActiveSpaceDidChangeNotification".into(), + callback, + ); } pub fn set_corner_radius(window: &WebviewWindow, radius: f64) { - let win: id = window.ns_window().unwrap() as _; + let win: id = window.ns_window().unwrap() as _; - unsafe { - let view: id = win.contentView(); + unsafe { + let view: id = win.contentView(); - view.wantsLayer(); + view.wantsLayer(); - let layer: id = view.layer(); + let layer: id = view.layer(); - let _: () = msg_send![layer, setCornerRadius: radius]; - } + let _: () = msg_send![layer, setCornerRadius: radius]; + } } pub fn position_menubar_panel(app_handle: &tauri::AppHandle, padding_top: f64) { - let window = app_handle.get_webview_window("panel").unwrap(); + let window = app_handle.get_webview_window("panel").unwrap(); - let monitor = monitor::get_monitor_with_cursor().unwrap(); + let monitor = monitor::get_monitor_with_cursor().unwrap(); - let scale_factor = monitor.scale_factor(); + let scale_factor = monitor.scale_factor(); - let visible_area = monitor.visible_area(); + let visible_area = monitor.visible_area(); - let monitor_pos = visible_area.position().to_logical::(scale_factor); + let monitor_pos = visible_area.position().to_logical::(scale_factor); - let monitor_size = visible_area.size().to_logical::(scale_factor); + let monitor_size = visible_area.size().to_logical::(scale_factor); - let mouse_location: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] }; + let mouse_location: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] }; - let handle: id = window.ns_window().unwrap() as _; + let handle: id = window.ns_window().unwrap() as _; - let mut win_frame: NSRect = unsafe { msg_send![handle, frame] }; + let mut win_frame: NSRect = unsafe { msg_send![handle, frame] }; - win_frame.origin.y = (monitor_pos.y + monitor_size.height) - win_frame.size.height; + win_frame.origin.y = (monitor_pos.y + monitor_size.height) - win_frame.size.height; - win_frame.origin.y -= padding_top; + win_frame.origin.y -= padding_top; - win_frame.origin.x = { - let top_right = mouse_location.x + (win_frame.size.width / 2.0); + win_frame.origin.x = { + let top_right = mouse_location.x + (win_frame.size.width / 2.0); - let is_offscreen = top_right > monitor_pos.x + monitor_size.width; + let is_offscreen = top_right > monitor_pos.x + monitor_size.width; - if !is_offscreen { - mouse_location.x - (win_frame.size.width / 2.0) - } else { - let diff = top_right - (monitor_pos.x + monitor_size.width); + if !is_offscreen { + mouse_location.x - (win_frame.size.width / 2.0) + } else { + let diff = top_right - (monitor_pos.x + monitor_size.width); - mouse_location.x - (win_frame.size.width / 2.0) - diff - } - }; + mouse_location.x - (win_frame.size.width / 2.0) - diff + } + }; - let _: () = unsafe { msg_send![handle, setFrame: win_frame display: NO] }; + let _: () = unsafe { msg_send![handle, setFrame: win_frame display: NO] }; } fn register_workspace_listener(name: String, callback: Box) { - let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] }; - let notification_center: id = unsafe { msg_send![workspace, notificationCenter] }; + let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] }; + let notification_center: id = unsafe { msg_send![workspace, notificationCenter] }; - let block = ConcreteBlock::new(move |_notif: id| { - callback(); - }); + let block = ConcreteBlock::new(move |_notif: id| { + callback(); + }); - let block = block.copy(); + let block = block.copy(); - let name: id = - unsafe { msg_send![class!(NSString), stringWithCString: CString::new(name).unwrap()] }; + let name: id = + unsafe { msg_send![class!(NSString), stringWithCString: CString::new(name).unwrap()] }; - unsafe { - let _: () = msg_send![ - notification_center, - addObserverForName: name object: nil queue: nil usingBlock: block - ]; - } + unsafe { + let _: () = msg_send![ + notification_center, + addObserverForName: name object: nil queue: nil usingBlock: block + ]; + } } fn app_pid() -> i32 { - let process_info: id = unsafe { msg_send![class!(NSProcessInfo), processInfo] }; - let pid: i32 = unsafe { msg_send![process_info, processIdentifier] }; + let process_info: id = unsafe { msg_send![class!(NSProcessInfo), processInfo] }; + let pid: i32 = unsafe { msg_send![process_info, processIdentifier] }; - pid + pid } fn get_frontmost_app_pid() -> i32 { - let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] }; - let frontmost_application: id = unsafe { msg_send![workspace, frontmostApplication] }; - let pid: i32 = unsafe { msg_send![frontmost_application, processIdentifier] }; + let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] }; + let frontmost_application: id = unsafe { msg_send![workspace, frontmostApplication] }; + let pid: i32 = unsafe { msg_send![frontmost_application, processIdentifier] }; - pid + pid } pub fn check_menubar_frontmost() -> bool { - get_frontmost_app_pid() == app_pid() + get_frontmost_app_pid() == app_pid() } diff --git a/src-tauri/src/commands/tray.rs b/src-tauri/src/commands/tray.rs index 4ab34a28..be3b58a5 100644 --- a/src-tauri/src/commands/tray.rs +++ b/src-tauri/src/commands/tray.rs @@ -1,65 +1,65 @@ use std::path::PathBuf; use tauri::window::{Effect, EffectsBuilder}; use tauri::{ - tray::{MouseButtonState, TrayIconEvent}, - WebviewWindowBuilder, + tray::{MouseButtonState, TrayIconEvent}, + WebviewWindowBuilder, }; use tauri::{AppHandle, Manager, WebviewUrl}; use tauri_nspanel::ManagerExt; use super::fns::{ - position_menubar_panel, set_corner_radius, setup_menubar_panel_listeners, - swizzle_to_menubar_panel, + position_menubar_panel, set_corner_radius, setup_menubar_panel_listeners, + swizzle_to_menubar_panel, }; pub fn create_tray_panel(account: &str, app: &AppHandle) { - let tray = app.tray_by_id("main").unwrap(); + let tray = app.tray_by_id("main").unwrap(); - tray.on_tray_icon_event(|tray, event| { - if let TrayIconEvent::Click { button_state, .. } = event { - if button_state == MouseButtonState::Up { - let app = tray.app_handle(); - let panel = app.get_webview_panel("panel").unwrap(); + tray.on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { button_state, .. } = event { + if button_state == MouseButtonState::Up { + let app = tray.app_handle(); + let panel = app.get_webview_panel("panel").unwrap(); - match panel.is_visible() { - true => panel.order_out(None), - false => { - position_menubar_panel(app, 0.0); - panel.show(); - } + match panel.is_visible() { + true => panel.order_out(None), + false => { + position_menubar_panel(app, 0.0); + panel.show(); + } + } + } } - } - } - }); + }); - if let Some(window) = app.get_webview_window("panel") { - let _ = window.destroy(); - }; + if let Some(window) = app.get_webview_window("panel") { + let _ = window.destroy(); + }; - let mut url = "/panel/".to_owned(); - url.push_str(account); + let mut url = "/panel/".to_owned(); + url.push_str(account); - let window = WebviewWindowBuilder::new(app, "panel", WebviewUrl::App(PathBuf::from(url))) - .title("Panel") - .inner_size(350.0, 500.0) - .fullscreen(false) - .resizable(false) - .visible(false) - .decorations(false) - .transparent(true) - .build() - .unwrap(); + let window = WebviewWindowBuilder::new(app, "panel", WebviewUrl::App(PathBuf::from(url))) + .title("Panel") + .inner_size(350.0, 500.0) + .fullscreen(false) + .resizable(false) + .visible(false) + .decorations(false) + .transparent(true) + .build() + .unwrap(); - let _ = window.set_effects( - EffectsBuilder::new() - .effect(Effect::Popover) - .state(tauri::window::EffectState::FollowsWindowActiveState) - .build(), - ); + let _ = window.set_effects( + EffectsBuilder::new() + .effect(Effect::Popover) + .state(tauri::window::EffectState::FollowsWindowActiveState) + .build(), + ); - set_corner_radius(&window, 13.0); + set_corner_radius(&window, 13.0); - // Convert window to panel - swizzle_to_menubar_panel(app); - setup_menubar_panel_listeners(app); + // Convert window to panel + swizzle_to_menubar_panel(app); + setup_menubar_panel_listeners(app); } diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 568b9088..24a8e46b 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -23,259 +23,260 @@ use crate::Nostr; #[derive(Serialize, Deserialize, Type)] pub struct Window { - label: String, - title: String, - url: String, - width: f64, - height: f64, - maximizable: bool, - minimizable: bool, - hidden_title: bool, + label: String, + title: String, + url: String, + width: f64, + height: f64, + maximizable: bool, + minimizable: bool, + hidden_title: bool, } #[derive(Serialize, Deserialize, Type)] pub struct Column { - label: String, - url: String, - x: f32, - y: f32, - width: f32, - height: f32, + label: String, + url: String, + x: f32, + y: f32, + width: f32, + height: f32, } #[tauri::command] #[specta::specta] pub fn create_column( - column: Column, - app_handle: tauri::AppHandle, - state: State<'_, Nostr>, + column: Column, + app_handle: tauri::AppHandle, + state: State<'_, Nostr>, ) -> Result { - let settings = state.settings.lock().unwrap().clone(); + let settings = state.settings.lock().unwrap().clone(); - match app_handle.get_window("main") { - Some(main_window) => match app_handle.get_webview(&column.label) { - Some(_) => Ok(column.label), - None => { - let path = PathBuf::from(column.url); - let webview_url = WebviewUrl::App(path); - let builder = match settings.proxy { - Some(url) => { - let proxy = Url::from_str(&url).unwrap(); - tauri::webview::WebviewBuilder::new(column.label, webview_url) - .user_agent("Lume/4.0") - .zoom_hotkeys_enabled(true) - .enable_clipboard_access() - .transparent(true) - .proxy_url(proxy) - } - None => tauri::webview::WebviewBuilder::new(column.label, webview_url) - .user_agent("Lume/4.0") - .zoom_hotkeys_enabled(true) - .enable_clipboard_access() - .transparent(true), - }; - match main_window.add_child( - builder, - LogicalPosition::new(column.x, column.y), - LogicalSize::new(column.width, column.height), - ) { - Ok(webview) => Ok(webview.label().into()), - Err(_) => Err("Create webview failed".into()), - } - } - }, - None => Err("Main window not found".into()), - } + match app_handle.get_window("main") { + Some(main_window) => match app_handle.get_webview(&column.label) { + Some(_) => Ok(column.label), + None => { + let path = PathBuf::from(column.url); + let webview_url = WebviewUrl::App(path); + let builder = match settings.proxy { + Some(url) => { + let proxy = Url::from_str(&url).unwrap(); + tauri::webview::WebviewBuilder::new(column.label, webview_url) + .user_agent("Lume/4.0") + .zoom_hotkeys_enabled(true) + .enable_clipboard_access() + .transparent(true) + .proxy_url(proxy) + } + None => tauri::webview::WebviewBuilder::new(column.label, webview_url) + .user_agent("Lume/4.0") + .zoom_hotkeys_enabled(true) + .enable_clipboard_access() + .transparent(true), + }; + match main_window.add_child( + builder, + LogicalPosition::new(column.x, column.y), + LogicalSize::new(column.width, column.height), + ) { + Ok(webview) => Ok(webview.label().into()), + Err(_) => Err("Create webview failed".into()), + } + } + }, + None => Err("Main window not found".into()), + } } #[tauri::command] #[specta::specta] pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result { - match app_handle.get_webview(label) { - Some(webview) => { - if webview.close().is_ok() { - Ok(true) - } else { - Ok(false) - } + match app_handle.get_webview(label) { + Some(webview) => { + if webview.close().is_ok() { + Ok(true) + } else { + Ok(false) + } + } + None => Err("Column not found.".into()), } - None => Err("Column not found.".into()), - } } #[tauri::command] #[specta::specta] pub fn reposition_column( - label: &str, - x: f32, - y: f32, - app_handle: tauri::AppHandle, + label: &str, + x: f32, + y: f32, + app_handle: tauri::AppHandle, ) -> Result<(), String> { - match app_handle.get_webview(label) { - Some(webview) => { - if webview.set_position(LogicalPosition::new(x, y)).is_ok() { - Ok(()) - } else { - Err("Reposition column failed".into()) - } + match app_handle.get_webview(label) { + Some(webview) => { + if webview.set_position(LogicalPosition::new(x, y)).is_ok() { + Ok(()) + } else { + Err("Reposition column failed".into()) + } + } + None => Err("Webview not found".into()), } - None => Err("Webview not found".into()), - } } #[tauri::command] #[specta::specta] pub fn resize_column( - label: &str, - width: f32, - height: f32, - app_handle: tauri::AppHandle, + label: &str, + width: f32, + height: f32, + app_handle: tauri::AppHandle, ) -> Result<(), String> { - match app_handle.get_webview(label) { - Some(webview) => { - if webview.set_size(LogicalSize::new(width, height)).is_ok() { - Ok(()) - } else { - Err("Resize column failed".into()) - } + match app_handle.get_webview(label) { + Some(webview) => { + if webview.set_size(LogicalSize::new(width, height)).is_ok() { + Ok(()) + } else { + Err("Resize column failed".into()) + } + } + None => Err("Webview not found".into()), } - None => Err("Webview not found".into()), - } } #[tauri::command] #[specta::specta] pub fn reload_column(label: &str, app_handle: tauri::AppHandle) -> Result<(), String> { - match app_handle.get_webview(label) { - Some(webview) => { - if webview.eval("window.location.reload()").is_ok() { - Ok(()) - } else { - Err("Reload column failed".into()) - } + match app_handle.get_webview(label) { + Some(webview) => { + if webview.eval("window.location.reload()").is_ok() { + Ok(()) + } else { + Err("Reload column failed".into()) + } + } + None => Err("Webview not found".into()), } - None => Err("Webview not found".into()), - } } #[tauri::command] #[specta::specta] pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> { - if let Some(window) = app_handle.get_window(&window.label) { - if window.is_visible().unwrap_or_default() { - let _ = window.set_focus(); + if let Some(window) = app_handle.get_window(&window.label) { + if window.is_visible().unwrap_or_default() { + let _ = window.set_focus(); + } else { + let _ = window.show(); + let _ = window.set_focus(); + }; } else { - let _ = window.show(); - let _ = window.set_focus(); - }; - } else { - #[cfg(target_os = "macos")] - let window = WebviewWindowBuilder::new( - &app_handle, - &window.label, - WebviewUrl::App(PathBuf::from(window.url)), - ) - .title(&window.title) - .min_inner_size(window.width, window.height) - .inner_size(window.width, window.height) - .hidden_title(window.hidden_title) - .title_bar_style(TitleBarStyle::Overlay) - .minimizable(window.minimizable) - .maximizable(window.maximizable) - .transparent(true) - .effects(WindowEffectsConfig { - state: None, - effects: vec![Effect::UnderWindowBackground], - radius: None, - color: None, - }) - .build() - .unwrap(); + #[cfg(target_os = "macos")] + let window = WebviewWindowBuilder::new( + &app_handle, + &window.label, + WebviewUrl::App(PathBuf::from(window.url)), + ) + .title(&window.title) + .min_inner_size(window.width, window.height) + .inner_size(window.width, window.height) + .hidden_title(window.hidden_title) + .title_bar_style(TitleBarStyle::Overlay) + .minimizable(window.minimizable) + .maximizable(window.maximizable) + .transparent(true) + .effects(WindowEffectsConfig { + state: None, + effects: vec![Effect::UnderWindowBackground], + radius: None, + color: None, + }) + .build() + .unwrap(); - #[cfg(target_os = "windows")] - let window = WebviewWindowBuilder::new( - &app_handle, - &window.label, - WebviewUrl::App(PathBuf::from(window.url)), - ) - .title(title) - .min_inner_size(window.width, window.height) - .inner_size(window.width, window.height) - .minimizable(window.minimizable) - .maximizable(window.maximizable) - .effects(WindowEffectsConfig { - state: None, - effects: vec![Effect::Mica], - radius: None, - color: None, - }) - .build() - .unwrap(); + #[cfg(target_os = "windows")] + let window = WebviewWindowBuilder::new( + &app_handle, + &window.label, + WebviewUrl::App(PathBuf::from(window.url)), + ) + .title(title) + .min_inner_size(window.width, window.height) + .inner_size(window.width, window.height) + .minimizable(window.minimizable) + .maximizable(window.maximizable) + .effects(WindowEffectsConfig { + state: None, + effects: vec![Effect::Mica], + radius: None, + color: None, + }) + .build() + .unwrap(); - #[cfg(target_os = "linux")] - let window = WebviewWindowBuilder::new( - &app_handle, - &window.label, - WebviewUrl::App(PathBuf::from(window.url)), - ) - .title(title) - .min_inner_size(window.width, window.height) - .inner_size(window.width, window.height) - .minimizable(window.minimizable) - .maximizable(window.maximizable) - .build() - .unwrap(); + #[cfg(target_os = "linux")] + let window = WebviewWindowBuilder::new( + &app_handle, + &window.label, + WebviewUrl::App(PathBuf::from(window.url)), + ) + .title(title) + .min_inner_size(window.width, window.height) + .inner_size(window.width, window.height) + .minimizable(window.minimizable) + .maximizable(window.maximizable) + .build() + .unwrap(); - // Set decoration - window.create_overlay_titlebar().unwrap(); + // Set decoration + window.create_overlay_titlebar().unwrap(); - // Restore native border - #[cfg(target_os = "macos")] - window.add_border(None); - } + // Restore native border + #[cfg(target_os = "macos")] + window.add_border(None); + } - Ok(()) + Ok(()) } #[tauri::command] #[specta::specta] pub fn open_main_window(app: tauri::AppHandle) { - if let Some(window) = app.get_window("main") { - if window.is_visible().unwrap_or_default() { - let _ = window.set_focus(); + if let Some(window) = app.get_window("main") { + if window.is_visible().unwrap_or_default() { + let _ = window.set_focus(); + } else { + let _ = window.show(); + let _ = window.set_focus(); + }; } else { - let _ = window.show(); - let _ = window.set_focus(); - }; - } else { - let window = WebviewWindowBuilder::from_config(&app, app.config().app.windows.first().unwrap()) - .unwrap() - .build() - .unwrap(); + let window = + WebviewWindowBuilder::from_config(&app, app.config().app.windows.first().unwrap()) + .unwrap() + .build() + .unwrap(); - // Restore native border - #[cfg(target_os = "macos")] - window.add_border(None); - } + // Restore native border + #[cfg(target_os = "macos")] + window.add_border(None); + } } #[tauri::command] #[specta::specta] pub fn force_quit() { - std::process::exit(0) + std::process::exit(0) } #[tauri::command] #[specta::specta] pub fn set_badge(count: i32) { - #[cfg(target_os = "macos")] - unsafe { - let label = if count == 0 { - nil - } else { - NSString::alloc(nil).init_str(&format!("{}", count)) - }; - let dock_tile: cocoa::base::id = msg_send![NSApp(), dockTile]; - let _: cocoa::base::id = msg_send![dock_tile, setBadgeLabel: label]; - } + #[cfg(target_os = "macos")] + unsafe { + let label = if count == 0 { + nil + } else { + NSString::alloc(nil).init_str(&format!("{}", count)) + }; + let dock_tile: cocoa::base::id = msg_send![NSApp(), dockTile]; + let _: cocoa::base::id = msg_send![dock_tile, setBadgeLabel: label]; + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 53f90ecd..b092da27 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,6 @@ #![cfg_attr( - all(not(debug_assertions), target_os = "windows"), - windows_subsystem = "windows" + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" )] #[cfg(target_os = "macos")] @@ -17,9 +17,9 @@ use specta::Type; use std::sync::Mutex; use std::time::Duration; use std::{ - fs, - io::{self, BufRead}, - str::FromStr, + fs, + io::{self, BufRead}, + str::FromStr, }; use tauri::{path::BaseDirectory, Manager}; #[cfg(not(target_os = "linux"))] @@ -30,39 +30,39 @@ pub mod nostr; #[derive(Serialize)] pub struct Nostr { - #[serde(skip_serializing)] - client: Client, - contact_list: Mutex>, - settings: Mutex, + #[serde(skip_serializing)] + client: Client, + contact_list: Mutex>, + settings: Mutex, } #[derive(Clone, Serialize, Deserialize, Type)] pub struct Settings { - proxy: Option, - image_resize_service: Option, - use_relay_hint: bool, - content_warning: bool, - display_avatar: bool, - display_zap_button: bool, - display_repost_button: bool, - display_media: bool, - vibrancy: bool, + proxy: Option, + image_resize_service: Option, + use_relay_hint: bool, + content_warning: bool, + display_avatar: bool, + display_zap_button: bool, + display_repost_button: bool, + display_media: bool, + vibrancy: bool, } impl Default for Settings { - fn default() -> Self { - Self { - proxy: None, - image_resize_service: Some("https://wsrv.nl/".into()), - use_relay_hint: true, - content_warning: true, - display_avatar: true, - display_zap_button: true, - display_repost_button: true, - display_media: true, - vibrancy: true, + fn default() -> Self { + Self { + proxy: None, + image_resize_service: Some("https://wsrv.nl/".into()), + use_relay_hint: true, + content_warning: true, + display_avatar: true, + display_zap_button: true, + display_repost_button: true, + display_media: true, + vibrancy: true, + } } - } } pub const FETCH_LIMIT: usize = 20; @@ -70,201 +70,190 @@ pub const NEWSFEED_NEG_LIMIT: usize = 256; pub const NOTIFICATION_NEG_LIMIT: usize = 64; fn main() { - let mut ctx = tauri::generate_context!(); + let mut ctx = tauri::generate_context!(); - let invoke_handler = { - let builder = tauri_specta::ts::builder().commands(tauri_specta::collect_commands![ - nostr::relay::get_relays, - nostr::relay::connect_relay, - nostr::relay::remove_relay, - nostr::relay::get_bootstrap_relays, - nostr::relay::save_bootstrap_relays, - nostr::keys::get_accounts, - nostr::keys::create_account, - nostr::keys::save_account, - nostr::keys::get_encrypted_key, - nostr::keys::get_private_key, - nostr::keys::connect_remote_account, - nostr::keys::load_account, - nostr::metadata::get_current_profile, - nostr::metadata::get_profile, - nostr::metadata::get_contact_list, - nostr::metadata::set_contact_list, - nostr::metadata::create_profile, - nostr::metadata::is_contact_list_empty, - nostr::metadata::check_contact, - nostr::metadata::toggle_contact, - nostr::metadata::get_nstore, - nostr::metadata::set_nstore, - nostr::metadata::set_wallet, - nostr::metadata::load_wallet, - nostr::metadata::remove_wallet, - nostr::metadata::zap_profile, - nostr::metadata::zap_event, - nostr::metadata::friend_to_friend, - nostr::metadata::get_notifications, - nostr::metadata::get_settings, - nostr::metadata::set_new_settings, - nostr::metadata::verify_nip05, - nostr::event::get_event_meta, - nostr::event::get_event, - nostr::event::get_event_from, - nostr::event::get_replies, - nostr::event::listen_event_reply, - nostr::event::get_events_by, - nostr::event::get_local_events, - nostr::event::listen_local_event, - nostr::event::get_group_events, - nostr::event::get_global_events, - nostr::event::get_hashtag_events, - nostr::event::publish, - nostr::event::reply, - nostr::event::repost, - nostr::event::event_to_bech32, - nostr::event::user_to_bech32, - nostr::event::unlisten, - commands::window::create_column, - commands::window::close_column, - commands::window::reposition_column, - commands::window::resize_column, - commands::window::reload_column, - commands::window::open_window, - commands::window::open_main_window, - commands::window::force_quit, - commands::window::set_badge - ]); + let invoke_handler = { + let builder = tauri_specta::ts::builder().commands(tauri_specta::collect_commands![ + nostr::relay::get_relays, + nostr::relay::connect_relay, + nostr::relay::remove_relay, + nostr::relay::get_bootstrap_relays, + nostr::relay::save_bootstrap_relays, + nostr::keys::get_accounts, + nostr::keys::create_account, + nostr::keys::save_account, + nostr::keys::get_encrypted_key, + nostr::keys::get_private_key, + nostr::keys::connect_remote_account, + nostr::keys::load_account, + nostr::metadata::get_current_profile, + nostr::metadata::get_profile, + nostr::metadata::get_contact_list, + nostr::metadata::set_contact_list, + nostr::metadata::create_profile, + nostr::metadata::is_contact_list_empty, + nostr::metadata::check_contact, + nostr::metadata::toggle_contact, + nostr::metadata::get_nstore, + nostr::metadata::set_nstore, + nostr::metadata::set_wallet, + nostr::metadata::load_wallet, + nostr::metadata::remove_wallet, + nostr::metadata::zap_profile, + nostr::metadata::zap_event, + nostr::metadata::friend_to_friend, + nostr::metadata::get_notifications, + nostr::metadata::get_settings, + nostr::metadata::set_new_settings, + nostr::metadata::verify_nip05, + nostr::event::get_event_meta, + nostr::event::get_event, + nostr::event::get_event_from, + nostr::event::get_replies, + nostr::event::listen_event_reply, + nostr::event::get_events_by, + nostr::event::get_local_events, + nostr::event::listen_local_event, + nostr::event::get_group_events, + nostr::event::get_global_events, + nostr::event::get_hashtag_events, + nostr::event::publish, + nostr::event::reply, + nostr::event::repost, + nostr::event::event_to_bech32, + nostr::event::user_to_bech32, + nostr::event::unlisten, + commands::window::create_column, + commands::window::close_column, + commands::window::reposition_column, + commands::window::resize_column, + commands::window::reload_column, + commands::window::open_window, + commands::window::open_main_window, + commands::window::force_quit, + commands::window::set_badge + ]); - #[cfg(debug_assertions)] - let builder = builder.path("../packages/system/src/commands.ts"); + #[cfg(debug_assertions)] + let builder = builder.path("../packages/system/src/commands.ts"); - builder.build().unwrap() - }; + builder.build().unwrap() + }; - tauri::Builder::default() - .setup(|app| { - #[cfg(target_os = "macos")] - app.handle().plugin(tauri_nspanel::init()).unwrap(); + tauri::Builder::default() + .setup(|app| { + #[cfg(target_os = "macos")] + app.handle().plugin(tauri_nspanel::init()).unwrap(); - #[cfg(not(target_os = "linux"))] - let main_window = app.get_webview_window("main").unwrap(); + #[cfg(not(target_os = "linux"))] + let main_window = app.get_webview_window("main").unwrap(); - // Set custom decoration for Windows - #[cfg(target_os = "windows")] - main_window.create_overlay_titlebar().unwrap(); + // Set custom decoration for Windows + #[cfg(target_os = "windows")] + main_window.create_overlay_titlebar().unwrap(); - // Restore native border - #[cfg(target_os = "macos")] - main_window.add_border(None); + // Restore native border + #[cfg(target_os = "macos")] + main_window.add_border(None); - // Set a custom inset to the traffic lights - #[cfg(target_os = "macos")] - main_window.set_traffic_lights_inset(8.0, 16.0).unwrap(); + // Set a custom inset to the traffic lights + #[cfg(target_os = "macos")] + main_window.set_traffic_lights_inset(8.0, 16.0).unwrap(); - // Create data folder if not exist - let home_dir = app.path().home_dir().unwrap(); - let _ = fs::create_dir_all(home_dir.join("Lume/")); + #[cfg(target_os = "macos")] + let win = main_window.clone(); - tauri::async_runtime::block_on(async move { - // Setup database - let database = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await; - - // Config - let opts = Options::new() - .automatic_authentication(true) - .connection_timeout(Some(Duration::from_secs(5))) - .timeout(Duration::from_secs(30)); - - // Setup nostr client - let client = match database { - Ok(db) => ClientBuilder::default().database(db).opts(opts).build(), - Err(_) => ClientBuilder::default().opts(opts).build(), - }; - - // Get bootstrap relays - if let Ok(path) = app - .path() - .resolve("resources/relays.txt", BaseDirectory::Resource) - { - let file = std::fs::File::open(&path).unwrap(); - let lines = io::BufReader::new(file).lines(); - - // Add bootstrap relays to relay pool - for line in lines.map_while(Result::ok) { - if let Some((relay, option)) = line.split_once(',') { - match RelayMetadata::from_str(option) { - Ok(meta) => { - println!("connecting to bootstrap relay...: {} - {}", relay, meta); - let opts = if meta == RelayMetadata::Read { - RelayOptions::new().read(true).write(false) - } else { - RelayOptions::new().write(true).read(false) - }; - let _ = client.add_relay_with_opts(relay, opts).await; + #[cfg(target_os = "macos")] + main_window.on_window_event(move |event| { + if let tauri::WindowEvent::ThemeChanged(_) = event { + win.set_traffic_lights_inset(8.0, 16.0).unwrap(); } - Err(_) => { - println!("connecting to bootstrap relay...: {}", relay); - let _ = client.add_relay(relay).await; + if let tauri::WindowEvent::Resized(_) = event { + win.set_traffic_lights_inset(8.0, 16.0).unwrap(); } - } - } - } - } + }); - // Connect - client.connect().await; + // Create data folder if not exist + let home_dir = app.path().home_dir().unwrap(); + let _ = fs::create_dir_all(home_dir.join("Lume/")); - // Update global state - app.handle().manage(Nostr { - client, - contact_list: Mutex::new(vec![]), - settings: Mutex::new(Settings::default()), + tauri::async_runtime::block_on(async move { + // Setup database + let database = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await; + + // Config + let opts = Options::new() + .automatic_authentication(true) + .connection_timeout(Some(Duration::from_secs(5))) + .timeout(Duration::from_secs(30)); + + // Setup nostr client + let client = match database { + Ok(db) => ClientBuilder::default().database(db).opts(opts).build(), + Err(_) => ClientBuilder::default().opts(opts).build(), + }; + + // Get bootstrap relays + if let Ok(path) = app + .path() + .resolve("resources/relays.txt", BaseDirectory::Resource) + { + let file = std::fs::File::open(&path).unwrap(); + let lines = io::BufReader::new(file).lines(); + + // Add bootstrap relays to relay pool + for line in lines.map_while(Result::ok) { + if let Some((relay, option)) = line.split_once(',') { + match RelayMetadata::from_str(option) { + Ok(meta) => { + println!( + "connecting to bootstrap relay...: {} - {}", + relay, meta + ); + let opts = if meta == RelayMetadata::Read { + RelayOptions::new().read(true).write(false) + } else { + RelayOptions::new().write(true).read(false) + }; + let _ = client.add_relay_with_opts(relay, opts).await; + } + Err(_) => { + println!("connecting to bootstrap relay...: {}", relay); + let _ = client.add_relay(relay).await; + } + } + } + } + } + + // Connect + client.connect().await; + + // Update global state + app.handle().manage(Nostr { + client, + contact_list: Mutex::new(vec![]), + settings: Mutex::new(Settings::default()), + }) + }); + + Ok(()) }) - }); - - Ok(()) - }) - .enable_macos_default_menu(false) - .on_window_event(move |window, event| { - #[cfg(target_os = "macos")] - if let tauri::WindowEvent::ThemeChanged(_) = event { - if let Some(webview) = window.get_webview_window(window.label()) { - webview.set_traffic_lights_inset(8.0, 16.0).unwrap(); - } - } - #[cfg(target_os = "macos")] - if let tauri::WindowEvent::Resized(_) = event { - if let Some(webview) = window.get_webview_window(window.label()) { - webview.set_traffic_lights_inset(8.0, 16.0).unwrap(); - } - } - }) - .plugin(tauri_nspanel::init()) - .plugin(tauri_plugin_theme::init(ctx.config_mut())) - .plugin(tauri_plugin_decorum::init()) - .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_http::init()) - .plugin(tauri_plugin_notification::init()) - .plugin(tauri_plugin_os::init()) - .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_upload::init()) - .plugin(tauri_plugin_updater::Builder::new().build()) - .plugin( - tauri_plugin_window_state::Builder::new() - .with_denylist(&["panel"]) - .build(), - ) - .invoke_handler(invoke_handler) - .build(ctx) - .expect("error while running tauri application") - .run(|_, event| { - if let tauri::RunEvent::ExitRequested { api, .. } = event { - // Hide app icon on macOS - // let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory); - // Keep API running - api.prevent_exit(); - } - }); + .enable_macos_default_menu(false) + .plugin(tauri_nspanel::init()) + .plugin(tauri_plugin_theme::init(ctx.config_mut())) + .plugin(tauri_plugin_decorum::init()) + .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_upload::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .invoke_handler(invoke_handler) + .run(ctx) + .expect("error while running tauri application"); } diff --git a/src-tauri/src/nostr/event.rs b/src-tauri/src/nostr/event.rs index a43afc6f..616ea81d 100644 --- a/src-tauri/src/nostr/event.rs +++ b/src-tauri/src/nostr/event.rs @@ -11,675 +11,676 @@ use crate::{Nostr, FETCH_LIMIT}; #[derive(Debug, Clone, Serialize, Type)] pub struct RichEvent { - pub raw: String, - pub parsed: Option, + pub raw: String, + pub parsed: Option, } #[tauri::command] #[specta::specta] pub async fn get_event_meta(content: &str) -> Result { - let meta = parse_event(content).await; - Ok(meta) + let meta = parse_event(content).await; + Ok(meta) } #[tauri::command] #[specta::specta] pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result { - 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 not valid.".into()), - }, - Err(_) => match EventId::from_hex(id) { - Ok(id) => id, - Err(_) => return Err("Event ID is not valid.".into()), - }, - }; + 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 not valid.".into()), + }, + Err(_) => match EventId::from_hex(id) { + Ok(id) => id, + Err(_) => return Err("Event ID is not valid.".into()), + }, + }; - match client - .get_events_of(vec![Filter::new().id(event_id)], None) - .await - { - Ok(events) => { - if let Some(event) = events.first() { - let raw = event.as_json(); - let parsed = if event.kind == Kind::TextNote { - Some(parse_event(&event.content).await) - } else { - None - }; + match client + .get_events_of(vec![Filter::new().id(event_id)], None) + .await + { + Ok(events) => { + if let Some(event) = events.first() { + let raw = event.as_json(); + let parsed = if event.kind == Kind::TextNote { + Some(parse_event(&event.content).await) + } else { + None + }; - Ok(RichEvent { raw, parsed }) - } else { - Err("Cannot found this event with current relay list".into()) - } + Ok(RichEvent { raw, parsed }) + } else { + Err("Cannot found this event with current relay list".into()) + } + } + Err(err) => Err(err.to_string()), } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn get_event_from( - id: &str, - relay_hint: &str, - state: State<'_, Nostr>, + id: &str, + relay_hint: &str, + state: State<'_, Nostr>, ) -> Result { - let client = &state.client; - let settings = state - .settings - .lock() - .map_err(|err| err.to_string())? - .clone(); + let client = &state.client; + let settings = state + .settings + .lock() + .map_err(|err| err.to_string())? + .clone(); - 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 not valid.".into()), - }, - Err(_) => match EventId::from_hex(id) { - Ok(id) => id, - Err(_) => return Err("Event ID is not valid.".into()), - }, - }; + 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 not valid.".into()), + }, + Err(_) => match EventId::from_hex(id) { + Ok(id) => id, + Err(_) => return Err("Event ID is not valid.".into()), + }, + }; - if !settings.use_relay_hint { - match client - .get_events_of(vec![Filter::new().id(event_id)], None) - .await - { - Ok(events) => { - if let Some(event) = events.first() { - let raw = event.as_json(); - let parsed = if event.kind == Kind::TextNote { - Some(parse_event(&event.content).await) - } else { - None - }; + if !settings.use_relay_hint { + match client + .get_events_of(vec![Filter::new().id(event_id)], None) + .await + { + Ok(events) => { + if let Some(event) = events.first() { + let raw = event.as_json(); + let parsed = if event.kind == Kind::TextNote { + Some(parse_event(&event.content).await) + } else { + None + }; - Ok(RichEvent { raw, parsed }) - } else { - Err("Cannot found this event with current relay list".into()) + Ok(RichEvent { raw, parsed }) + } else { + Err("Cannot found this event with current relay list".into()) + } + } + Err(err) => Err(err.to_string()), } - } - Err(err) => Err(err.to_string()), - } - } else { - // Add relay hint to relay pool - if let Err(err) = client.add_relay(relay_hint).await { - return Err(err.to_string()); - } - - if client.connect_relay(relay_hint).await.is_ok() { - match client - .get_events_from(vec![relay_hint], vec![Filter::new().id(event_id)], None) - .await - { - Ok(events) => { - if let Some(event) = events.first() { - let raw = event.as_json(); - let parsed = if event.kind == Kind::TextNote { - Some(parse_event(&event.content).await) - } else { - None - }; - - Ok(RichEvent { raw, parsed }) - } else { - Err("Cannot found this event with current relay list".into()) - } - } - Err(err) => Err(err.to_string()), - } } else { - Err("Relay connection failed.".into()) + // Add relay hint to relay pool + if let Err(err) = client.add_relay(relay_hint).await { + return Err(err.to_string()); + } + + if client.connect_relay(relay_hint).await.is_ok() { + match client + .get_events_from(vec![relay_hint], vec![Filter::new().id(event_id)], None) + .await + { + Ok(events) => { + if let Some(event) = events.first() { + let raw = event.as_json(); + let parsed = if event.kind == Kind::TextNote { + Some(parse_event(&event.content).await) + } else { + None + }; + + Ok(RichEvent { raw, parsed }) + } else { + Err("Cannot found this event with current relay list".into()) + } + } + Err(err) => Err(err.to_string()), + } + } else { + Err("Relay connection failed.".into()) + } } - } } #[tauri::command] #[specta::specta] pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result, String> { - let client = &state.client; + let client = &state.client; - let event_id = match EventId::from_hex(id) { - Ok(id) => id, - Err(err) => return Err(err.to_string()), - }; + let event_id = match EventId::from_hex(id) { + Ok(id) => id, + Err(err) => return Err(err.to_string()), + }; - let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id); + let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id); - match client.get_events_of(vec![filter], None).await { - Ok(events) => { - let futures = events.into_iter().map(|ev| async move { - let raw = ev.as_json(); - let parsed = if ev.kind == Kind::TextNote { - Some(parse_event(&ev.content).await) - } else { - None - }; + match client.get_events_of(vec![filter], None).await { + Ok(events) => { + let futures = events.into_iter().map(|ev| async move { + let raw = ev.as_json(); + let parsed = if ev.kind == Kind::TextNote { + Some(parse_event(&ev.content).await) + } else { + None + }; - RichEvent { raw, parsed } - }); - let rich_events = join_all(futures).await; + RichEvent { raw, parsed } + }); + let rich_events = join_all(futures).await; - Ok(rich_events) + Ok(rich_events) + } + Err(err) => Err(err.to_string()), } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn listen_event_reply(id: &str, state: State<'_, Nostr>) -> Result<(), String> { - let client = &state.client; + let client = &state.client; - let mut label = "event-".to_owned(); - label.push_str(id); + let mut label = "event-".to_owned(); + label.push_str(id); - let sub_id = SubscriptionId::new(label); - let event_id = match EventId::from_hex(id) { - Ok(id) => id, - Err(err) => return Err(err.to_string()), - }; - let filter = Filter::new() - .kinds(vec![Kind::TextNote]) - .event(event_id) - .since(Timestamp::now()); + let sub_id = SubscriptionId::new(label); + let event_id = match EventId::from_hex(id) { + Ok(id) => id, + Err(err) => return Err(err.to_string()), + }; + let filter = Filter::new() + .kinds(vec![Kind::TextNote]) + .event(event_id) + .since(Timestamp::now()); - // Subscribe - client.subscribe_with_id(sub_id, vec![filter], None).await; + // Subscribe + let _ = client.subscribe_with_id(sub_id, vec![filter], None).await; - Ok(()) + Ok(()) } #[tauri::command] #[specta::specta] pub async fn get_events_by( - public_key: &str, - as_of: Option<&str>, - state: State<'_, Nostr>, + public_key: &str, + as_of: Option<&str>, + state: State<'_, Nostr>, ) -> Result, String> { - let client = &state.client; + let client = &state.client; - match PublicKey::from_str(public_key) { - Ok(author) => { - let until = match as_of { - Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, - None => Timestamp::now(), - }; - let filter = Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .author(author) - .limit(FETCH_LIMIT) - .until(until); - - match client.get_events_of(vec![filter], None).await { - Ok(events) => { - let futures = events.into_iter().map(|ev| async move { - let raw = ev.as_json(); - let parsed = if ev.kind == Kind::TextNote { - Some(parse_event(&ev.content).await) - } else { - None + match PublicKey::from_str(public_key) { + Ok(author) => { + let until = match as_of { + Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, + None => Timestamp::now(), }; + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .author(author) + .limit(FETCH_LIMIT) + .until(until); - RichEvent { raw, parsed } - }); - let rich_events = join_all(futures).await; + match client.get_events_of(vec![filter], None).await { + Ok(events) => { + let futures = events.into_iter().map(|ev| async move { + let raw = ev.as_json(); + let parsed = if ev.kind == Kind::TextNote { + Some(parse_event(&ev.content).await) + } else { + None + }; - Ok(rich_events) + RichEvent { raw, parsed } + }); + let rich_events = join_all(futures).await; + + Ok(rich_events) + } + Err(err) => Err(err.to_string()), + } } Err(err) => Err(err.to_string()), - } } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn get_local_events( - until: Option<&str>, - state: State<'_, Nostr>, + until: Option<&str>, + state: State<'_, Nostr>, ) -> Result, String> { - let client = &state.client; - let contact_list = state - .contact_list - .lock() - .map_err(|err| err.to_string())? - .clone(); + let client = &state.client; + let contact_list = state + .contact_list + .lock() + .map_err(|err| err.to_string())? + .clone(); - let as_of = match until { - Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, - None => Timestamp::now(), - }; + let as_of = match until { + Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, + None => Timestamp::now(), + }; - let authors: Vec = contact_list.into_iter().map(|f| f.public_key).collect(); + let authors: Vec = contact_list.into_iter().map(|f| f.public_key).collect(); - let filter = Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(64) - .until(as_of) - .authors(authors); + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(64) + .until(as_of) + .authors(authors); - match client.database().query(vec![filter], Order::Desc).await { - Ok(events) => { - let dedup = dedup_event(&events); - let futures = dedup.into_iter().map(|ev| async move { - let raw = ev.as_json(); - let parsed = if ev.kind == Kind::TextNote { - Some(parse_event(&ev.content).await) - } else { - None - }; + match client.database().query(vec![filter], Order::Desc).await { + Ok(events) => { + let dedup = dedup_event(&events); + let futures = dedup.into_iter().map(|ev| async move { + let raw = ev.as_json(); + let parsed = if ev.kind == Kind::TextNote { + Some(parse_event(&ev.content).await) + } else { + None + }; - RichEvent { raw, parsed } - }); - let rich_events = join_all(futures).await; + RichEvent { raw, parsed } + }); + let rich_events = join_all(futures).await; - Ok(rich_events) + Ok(rich_events) + } + Err(err) => Err(err.to_string()), } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn listen_local_event(label: &str, state: State<'_, Nostr>) -> Result<(), String> { - let client = &state.client; + let client = &state.client; - let contact_list = state - .contact_list - .lock() - .map_err(|err| err.to_string())? - .clone(); + let contact_list = state + .contact_list + .lock() + .map_err(|err| err.to_string())? + .clone(); - let authors: Vec = contact_list.into_iter().map(|f| f.public_key).collect(); - let sub_id = SubscriptionId::new(label); + let authors: Vec = contact_list.into_iter().map(|f| f.public_key).collect(); + let sub_id = SubscriptionId::new(label); - let filter = Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .authors(authors) - .since(Timestamp::now()); + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .authors(authors) + .since(Timestamp::now()); - // Subscribe - client.subscribe_with_id(sub_id, vec![filter], None).await; + // Subscribe + let _ = client.subscribe_with_id(sub_id, vec![filter], None).await; - Ok(()) + Ok(()) } #[tauri::command] #[specta::specta] pub async fn get_group_events( - public_keys: Vec<&str>, - until: Option<&str>, - state: State<'_, Nostr>, + public_keys: Vec<&str>, + until: Option<&str>, + state: State<'_, Nostr>, ) -> Result, String> { - let client = &state.client; + let client = &state.client; - let as_of = match until { - Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, - None => Timestamp::now(), - }; + let as_of = match until { + Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, + None => Timestamp::now(), + }; - let authors: Vec = public_keys - .into_iter() - .map(|p| { - if p.starts_with("npub1") { - PublicKey::from_bech32(p).map_err(|err| err.to_string()) - } else { - PublicKey::from_hex(p).map_err(|err| err.to_string()) - } - }) - .collect::, _>>()?; + let authors: Vec = public_keys + .into_iter() + .map(|p| { + if p.starts_with("npub1") { + PublicKey::from_bech32(p).map_err(|err| err.to_string()) + } else { + PublicKey::from_hex(p).map_err(|err| err.to_string()) + } + }) + .collect::, _>>()?; - let filter = Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(FETCH_LIMIT) - .until(as_of) - .authors(authors); + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(FETCH_LIMIT) + .until(as_of) + .authors(authors); - match client - .get_events_of(vec![filter], Some(Duration::from_secs(10))) - .await - { - Ok(events) => { - let dedup = dedup_event(&events); + match client + .get_events_of(vec![filter], Some(Duration::from_secs(10))) + .await + { + Ok(events) => { + let dedup = dedup_event(&events); - let futures = dedup.into_iter().map(|ev| async move { - let raw = ev.as_json(); - let parsed = if ev.kind == Kind::TextNote { - Some(parse_event(&ev.content).await) - } else { - None - }; + let futures = dedup.into_iter().map(|ev| async move { + let raw = ev.as_json(); + let parsed = if ev.kind == Kind::TextNote { + Some(parse_event(&ev.content).await) + } else { + None + }; - RichEvent { raw, parsed } - }); + RichEvent { raw, parsed } + }); - let rich_events = join_all(futures).await; + let rich_events = join_all(futures).await; - Ok(rich_events) + Ok(rich_events) + } + Err(err) => Err(err.to_string()), } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn get_global_events( - until: Option<&str>, - state: State<'_, Nostr>, + until: Option<&str>, + state: State<'_, Nostr>, ) -> Result, String> { - let client = &state.client; - let as_of = match until { - Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, - None => Timestamp::now(), - }; + let client = &state.client; + let as_of = match until { + Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, + None => Timestamp::now(), + }; - let filter = Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(FETCH_LIMIT) - .until(as_of); + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(FETCH_LIMIT) + .until(as_of); - match client - .get_events_of(vec![filter], Some(Duration::from_secs(8))) - .await - { - Ok(events) => { - let dedup = dedup_event(&events); - let futures = dedup.into_iter().map(|ev| async move { - let raw = ev.as_json(); - let parsed = if ev.kind == Kind::TextNote { - Some(parse_event(&ev.content).await) - } else { - None - }; + match client + .get_events_of(vec![filter], Some(Duration::from_secs(8))) + .await + { + Ok(events) => { + let dedup = dedup_event(&events); + let futures = dedup.into_iter().map(|ev| async move { + let raw = ev.as_json(); + let parsed = if ev.kind == Kind::TextNote { + Some(parse_event(&ev.content).await) + } else { + None + }; - RichEvent { raw, parsed } - }); - let rich_events = join_all(futures).await; + RichEvent { raw, parsed } + }); + let rich_events = join_all(futures).await; - Ok(rich_events) + Ok(rich_events) + } + Err(err) => Err(err.to_string()), } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn get_hashtag_events( - hashtags: Vec<&str>, - until: Option<&str>, - state: State<'_, Nostr>, + hashtags: Vec<&str>, + until: Option<&str>, + state: State<'_, Nostr>, ) -> Result, String> { - let client = &state.client; - let as_of = match until { - Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, - None => Timestamp::now(), - }; - let filter = Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(FETCH_LIMIT) - .until(as_of) - .hashtags(hashtags); + let client = &state.client; + let as_of = match until { + Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?, + None => Timestamp::now(), + }; + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(FETCH_LIMIT) + .until(as_of) + .hashtags(hashtags); - match client.get_events_of(vec![filter], None).await { - Ok(events) => { - let dedup = dedup_event(&events); - let futures = dedup.into_iter().map(|ev| async move { - let raw = ev.as_json(); - let parsed = if ev.kind == Kind::TextNote { - Some(parse_event(&ev.content).await) - } else { - None - }; + match client.get_events_of(vec![filter], None).await { + Ok(events) => { + let dedup = dedup_event(&events); + let futures = dedup.into_iter().map(|ev| async move { + let raw = ev.as_json(); + let parsed = if ev.kind == Kind::TextNote { + Some(parse_event(&ev.content).await) + } else { + None + }; - RichEvent { raw, parsed } - }); - let rich_events = join_all(futures).await; + RichEvent { raw, parsed } + }); + let rich_events = join_all(futures).await; - Ok(rich_events) + Ok(rich_events) + } + Err(err) => Err(err.to_string()), } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn publish( - content: String, - warning: Option, - difficulty: Option, - state: State<'_, Nostr>, + content: String, + warning: Option, + difficulty: Option, + state: State<'_, Nostr>, ) -> Result { - let client = &state.client; + let client = &state.client; - // Create tags from content - let mut tags = create_event_tags(&content); + // Create tags from content + let mut tags = create_event_tags(&content); - // Add content-warning tag if present - if let Some(reason) = warning { - let t = TagStandard::ContentWarning { - reason: Some(reason), + // Add content-warning tag if present + if let Some(reason) = warning { + let t = TagStandard::ContentWarning { + reason: Some(reason), + }; + let tag = Tag::from(t); + tags.push(tag) }; - let tag = Tag::from(t); - tags.push(tag) - }; - // Get signer - let signer = match client.signer().await { - Ok(signer) => signer, - Err(_) => return Err("Signer is required.".into()), - }; + // Get signer + let signer = match client.signer().await { + Ok(signer) => signer, + Err(_) => return Err("Signer is required.".into()), + }; - // Get public key - let public_key = signer.public_key().await.map_err(|err| err.to_string())?; + // Get public key + let public_key = signer.public_key().await.map_err(|err| err.to_string())?; - // Create unsigned event - let unsigned_event = match difficulty { - Some(num) => EventBuilder::text_note(content, tags).to_unsigned_pow_event(public_key, num), - None => EventBuilder::text_note(content, tags).to_unsigned_event(public_key), - }; + // Create unsigned event + let unsigned_event = match difficulty { + Some(num) => EventBuilder::text_note(content, tags).to_unsigned_pow_event(public_key, num), + None => EventBuilder::text_note(content, tags).to_unsigned_event(public_key), + }; - // Publish - match signer.sign_event(unsigned_event).await { - Ok(event) => match client.send_event(event).await { - Ok(event_id) => Ok(event_id.to_bech32().map_err(|err| err.to_string())?), - Err(err) => Err(err.to_string()), - }, - Err(err) => Err(err.to_string()), - } + // Publish + match signer.sign_event(unsigned_event).await { + Ok(event) => match client.send_event(event).await { + Ok(event_id) => Ok(event_id.to_bech32().map_err(|err| err.to_string())?), + Err(err) => Err(err.to_string()), + }, + Err(err) => Err(err.to_string()), + } } #[tauri::command] #[specta::specta] pub async fn reply( - content: String, - to: String, - root: Option, - state: State<'_, Nostr>, + content: String, + to: String, + root: Option, + state: State<'_, Nostr>, ) -> Result { - let client = &state.client; - let database = client.database(); + let client = &state.client; + let database = client.database(); - // Create tags from content - let mut tags = create_event_tags(&content); + // Create tags from content + let mut tags = create_event_tags(&content); - let reply_id = match EventId::from_hex(to) { - Ok(val) => val, - Err(_) => return Err("Event is not valid.".into()), - }; - - match database - .query(vec![Filter::new().id(reply_id)], Order::Desc) - .await - { - Ok(events) => { - if let Some(event) = events.into_iter().next() { - let relay_hint = if let Some(relays) = database - .event_seen_on_relays(event.id) - .await - .map_err(|err| err.to_string())? - { - relays.into_iter().next().map(UncheckedUrl::new) - } else { - None - }; - let t = TagStandard::Event { - event_id: event.id, - relay_url: relay_hint, - marker: Some(Marker::Reply), - public_key: Some(event.pubkey), - }; - let tag = Tag::from(t); - tags.push(tag) - } else { - return Err("Reply event is not found.".into()); - } - } - Err(err) => return Err(err.to_string()), - }; - - if let Some(id) = root { - let root_id = match EventId::from_hex(id) { - Ok(val) => val, - Err(_) => return Err("Event is not valid.".into()), + let reply_id = match EventId::from_hex(to) { + Ok(val) => val, + Err(_) => return Err("Event is not valid.".into()), }; - if let Ok(events) = database - .query(vec![Filter::new().id(root_id)], Order::Desc) - .await + match database + .query(vec![Filter::new().id(reply_id)], Order::Desc) + .await { - if let Some(event) = events.into_iter().next() { - let relay_hint = if let Some(relays) = database - .event_seen_on_relays(event.id) - .await - .map_err(|err| err.to_string())? - { - relays.into_iter().next().map(UncheckedUrl::new) - } else { - None - }; - let t = TagStandard::Event { - event_id: event.id, - relay_url: relay_hint, - marker: Some(Marker::Root), - public_key: Some(event.pubkey), - }; - let tag = Tag::from(t); - tags.push(tag) - } - } - }; + Ok(events) => { + if let Some(event) = events.into_iter().next() { + let relay_hint = if let Some(relays) = database + .event_seen_on_relays(event.id) + .await + .map_err(|err| err.to_string())? + { + relays.into_iter().next().map(UncheckedUrl::new) + } else { + None + }; + let t = TagStandard::Event { + event_id: event.id, + relay_url: relay_hint, + marker: Some(Marker::Reply), + public_key: Some(event.pubkey), + }; + let tag = Tag::from(t); + tags.push(tag) + } else { + return Err("Reply event is not found.".into()); + } + } + Err(err) => return Err(err.to_string()), + }; - match client.publish_text_note(content, tags).await { - Ok(event_id) => Ok(event_id.to_bech32().map_err(|err| err.to_string())?), - Err(err) => Err(err.to_string()), - } + if let Some(id) = root { + let root_id = match EventId::from_hex(id) { + Ok(val) => val, + Err(_) => return Err("Event is not valid.".into()), + }; + + if let Ok(events) = database + .query(vec![Filter::new().id(root_id)], Order::Desc) + .await + { + if let Some(event) = events.into_iter().next() { + let relay_hint = if let Some(relays) = database + .event_seen_on_relays(event.id) + .await + .map_err(|err| err.to_string())? + { + relays.into_iter().next().map(UncheckedUrl::new) + } else { + None + }; + let t = TagStandard::Event { + event_id: event.id, + relay_url: relay_hint, + marker: Some(Marker::Root), + public_key: Some(event.pubkey), + }; + let tag = Tag::from(t); + tags.push(tag) + } + } + }; + + match client.publish_text_note(content, tags).await { + Ok(event_id) => Ok(event_id.to_bech32().map_err(|err| err.to_string())?), + Err(err) => Err(err.to_string()), + } } #[tauri::command] #[specta::specta] pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; - let event = Event::from_json(raw).map_err(|err| err.to_string())?; + let client = &state.client; + let event = Event::from_json(raw).map_err(|err| err.to_string())?; - match client.repost(&event, None).await { - Ok(event_id) => Ok(event_id.to_string()), - Err(err) => Err(err.to_string()), - } + match client.repost(&event, None).await { + Ok(event_id) => Ok(event_id.to_string()), + Err(err) => Err(err.to_string()), + } } #[tauri::command] #[specta::specta] pub async fn event_to_bech32(id: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; + let client = &state.client; - let event_id = match EventId::from_hex(id) { - Ok(id) => id, - Err(_) => return Err("ID is not valid.".into()), - }; + let event_id = match EventId::from_hex(id) { + Ok(id) => id, + Err(_) => return Err("ID is not valid.".into()), + }; - let seens = client - .database() - .event_seen_on_relays(event_id) - .await - .map_err(|err| err.to_string())?; + let seens = client + .database() + .event_seen_on_relays(event_id) + .await + .map_err(|err| err.to_string())?; - match seens { - Some(set) => { - let relays = set.into_iter().collect::>(); - let event = Nip19Event::new(event_id, relays); + match seens { + Some(set) => { + let relays = set.into_iter().collect::>(); + let event = Nip19Event::new(event_id, relays); - match event.to_bech32() { - Ok(id) => Ok(id), - Err(err) => Err(err.to_string()), - } + match event.to_bech32() { + Ok(id) => Ok(id), + Err(err) => Err(err.to_string()), + } + } + None => match event_id.to_bech32() { + Ok(id) => Ok(id), + Err(err) => Err(err.to_string()), + }, } - None => match event_id.to_bech32() { - Ok(id) => Ok(id), - Err(err) => Err(err.to_string()), - }, - } } #[tauri::command] #[specta::specta] pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; + let client = &state.client; - let public_key = match PublicKey::from_str(user) { - Ok(pk) => pk, - Err(_) => return Err("Public Key is not valid.".into()), - }; + let public_key = match PublicKey::from_str(user) { + Ok(pk) => pk, + Err(_) => return Err("Public Key is not valid.".into()), + }; - match client - .get_events_of( - vec![Filter::new() - .author(public_key) - .kind(Kind::RelayList) - .limit(1)], - Some(Duration::from_secs(10)), - ) - .await - { - Ok(events) => match events.first() { - Some(event) => { - let relay_list = nip65::extract_relay_list(event); - let relays = relay_list - .into_iter() - .map(|i| i.0.to_string()) - .collect::>(); - let profile = Nip19Profile::new(public_key, relays).map_err(|err| err.to_string())?; + match client + .get_events_of( + vec![Filter::new() + .author(public_key) + .kind(Kind::RelayList) + .limit(1)], + Some(Duration::from_secs(10)), + ) + .await + { + Ok(events) => match events.first() { + Some(event) => { + let relay_list = nip65::extract_relay_list(event); + let relays = relay_list + .into_iter() + .map(|i| i.0.to_string()) + .collect::>(); + let profile = + Nip19Profile::new(public_key, relays).map_err(|err| err.to_string())?; - Ok(profile.to_bech32().map_err(|err| err.to_string())?) - } - None => match public_key.to_bech32() { - Ok(pk) => Ok(pk), - Err(err) => Err(err.to_string()), - }, - }, - Err(_) => match public_key.to_bech32() { - Ok(pk) => Ok(pk), - Err(err) => Err(err.to_string()), - }, - } + Ok(profile.to_bech32().map_err(|err| err.to_string())?) + } + None => match public_key.to_bech32() { + Ok(pk) => Ok(pk), + Err(err) => Err(err.to_string()), + }, + }, + Err(_) => match public_key.to_bech32() { + Ok(pk) => Ok(pk), + Err(err) => Err(err.to_string()), + }, + } } #[tauri::command] #[specta::specta] pub async fn unlisten(id: &str, state: State<'_, Nostr>) -> Result<(), ()> { - let client = &state.client; - let sub_id = SubscriptionId::new(id); + let client = &state.client; + let sub_id = SubscriptionId::new(id); - // Remove subscription - client.unsubscribe(sub_id).await; + // Remove subscription + client.unsubscribe(sub_id).await; - Ok(()) + Ok(()) } diff --git a/src-tauri/src/nostr/internal.rs b/src-tauri/src/nostr/internal.rs index 0b4aaffc..63d4d04f 100644 --- a/src-tauri/src/nostr/internal.rs +++ b/src-tauri/src/nostr/internal.rs @@ -2,74 +2,74 @@ use crate::Settings; use nostr_sdk::prelude::*; pub async fn init_nip65(client: &Client) { - let signer = client.signer().await.unwrap(); - let public_key = signer.public_key().await.unwrap(); + let signer = client.signer().await.unwrap(); + let public_key = signer.public_key().await.unwrap(); - if let Ok(events) = client - .get_events_of( - vec![Filter::new() - .author(public_key) - .kind(Kind::RelayList) - .limit(1)], - None, - ) - .await - { - if let Some(event) = events.first() { - let relay_list = nip65::extract_relay_list(event); - for item in relay_list.into_iter() { - let relay_url = item.0.to_string(); - let opts = match item.1 { - Some(val) => { - if val == &RelayMetadata::Read { - RelayOptions::new().read(true).write(false) - } else { - RelayOptions::new().write(true).read(false) + if let Ok(events) = client + .get_events_of( + vec![Filter::new() + .author(public_key) + .kind(Kind::RelayList) + .limit(1)], + None, + ) + .await + { + if let Some(event) = events.first() { + let relay_list = nip65::extract_relay_list(event); + for item in relay_list.into_iter() { + let relay_url = item.0.to_string(); + let opts = match item.1 { + Some(val) => { + if val == &RelayMetadata::Read { + RelayOptions::new().read(true).write(false) + } else { + RelayOptions::new().write(true).read(false) + } + } + None => RelayOptions::default(), + }; + + // Add relay to relay pool + let _ = client + .add_relay_with_opts(&relay_url, opts) + .await + .unwrap_or_default(); + + // Connect relay + client.connect_relay(relay_url).await.unwrap_or_default(); + println!("connecting to relay: {} - {:?}", item.0, item.1); } - } - None => RelayOptions::default(), - }; - - // Add relay to relay pool - let _ = client - .add_relay_with_opts(&relay_url, opts) - .await - .unwrap_or_default(); - - // Connect relay - client.connect_relay(relay_url).await.unwrap_or_default(); - println!("connecting to relay: {} - {:?}", item.0, item.1); - } - } - }; + } + }; } pub async fn get_user_settings(client: &Client) -> Result { - let ident = "lume:settings"; - let signer = client.signer().await.unwrap(); - let public_key = signer.public_key().await.unwrap(); + let ident = "lume:settings"; + let signer = client.signer().await.unwrap(); + let public_key = signer.public_key().await.unwrap(); - let filter = Filter::new() - .author(public_key) - .kind(Kind::ApplicationSpecificData) - .identifier(ident) - .limit(1); + let filter = Filter::new() + .author(public_key) + .kind(Kind::ApplicationSpecificData) + .identifier(ident) + .limit(1); - if let Ok(events) = client.get_events_of(vec![filter], None).await { - if let Some(event) = events.first() { - let content = event.content(); - if let Ok(decrypted) = signer.nip44_decrypt(public_key, content).await { - match serde_json::from_str(&decrypted) { - Ok(parsed) => parsed, - Err(_) => Err("Could not parse settings payload".into()), + if let Ok(events) = client.get_events_of(vec![filter], None).await { + if let Some(event) = events.first() { + let content = event.content(); + if let Ok(decrypted) = signer.nip44_decrypt(public_key, content).await { + match serde_json::from_str(&decrypted) { + Ok(parsed) => parsed, + Err(_) => Err("Could not parse settings payload".into()), + } + } else { + Err("Decrypt settings failed.".into()) + } + } else { + Err("Settings not found.".into()) } - } else { - Err("Decrypt settings failed.".into()) - } } else { - Err("Settings not found.".into()) + Err("Settings not found.".into()) } - } else { - Err("Settings not found.".into()) - } } diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index 6778a8a4..1a4a3eea 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -1,3 +1,7 @@ +use crate::nostr::event::RichEvent; +use crate::nostr::internal::{get_user_settings, init_nip65}; +use crate::nostr::utils::parse_event; +use crate::{Nostr, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT}; use keyring::Entry; use keyring_search::{Limit, List, Search}; use nostr_sdk::prelude::*; @@ -8,402 +12,411 @@ use std::time::Duration; use tauri::{Emitter, EventTarget, Manager, State}; use tauri_plugin_notification::NotificationExt; -use crate::commands::tray::create_tray_panel; -use crate::nostr::event::RichEvent; -use crate::nostr::internal::{get_user_settings, init_nip65}; -use crate::nostr::utils::parse_event; -use crate::{Nostr, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT}; - #[derive(Serialize, Type)] pub struct Account { - npub: String, - nsec: String, + npub: String, + nsec: String, } #[tauri::command] #[specta::specta] pub fn get_accounts() -> Result, String> { - let search = Search::new().map_err(|e| e.to_string())?; - let results = search.by("Account", "nostr_secret"); + let search = Search::new().map_err(|e| e.to_string())?; + let results = search.by("Account", "nostr_secret"); - match List::list_credentials(results, Limit::All) { - Ok(list) => { - let accounts: HashSet = list - .split_whitespace() - .filter(|v| v.starts_with("npub1")) - .map(String::from) - .collect(); + match List::list_credentials(results, Limit::All) { + Ok(list) => { + let accounts: HashSet = list + .split_whitespace() + .filter(|v| v.starts_with("npub1")) + .map(String::from) + .collect(); - Ok(accounts.into_iter().collect()) + Ok(accounts.into_iter().collect()) + } + Err(_) => Err("Empty.".into()), } - Err(_) => Err("Empty.".into()), - } } #[tauri::command] #[specta::specta] pub fn create_account() -> Result { - let keys = Keys::generate(); - let public_key = keys.public_key(); - let secret_key = keys.secret_key().unwrap(); + let keys = Keys::generate(); + let public_key = keys.public_key(); + let secret_key = keys.secret_key().unwrap(); - let result = Account { - npub: public_key.to_bech32().unwrap(), - nsec: secret_key.to_bech32().unwrap(), - }; + let result = Account { + npub: public_key.to_bech32().unwrap(), + nsec: secret_key.to_bech32().unwrap(), + }; - Ok(result) + Ok(result) } #[tauri::command] #[specta::specta] pub fn get_private_key(npub: &str) -> Result { - let keyring = Entry::new(npub, "nostr_secret").unwrap(); + let keyring = Entry::new(npub, "nostr_secret").unwrap(); - if let Ok(nsec) = keyring.get_password() { - let secret_key = SecretKey::from_bech32(nsec).unwrap(); - Ok(secret_key.to_bech32().unwrap()) - } else { - Err("Key not found".into()) - } + if let Ok(nsec) = keyring.get_password() { + let secret_key = SecretKey::from_bech32(nsec).unwrap(); + Ok(secret_key.to_bech32().unwrap()) + } else { + Err("Key not found".into()) + } } #[tauri::command] #[specta::specta] pub async fn save_account( - nsec: &str, - password: &str, - state: State<'_, Nostr>, + nsec: &str, + password: &str, + state: State<'_, Nostr>, ) -> Result { - let secret_key = if nsec.starts_with("ncryptsec") { - let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap(); - encrypted_key - .to_secret_key(password) - .map_err(|err| err.to_string()) - } else { - SecretKey::from_bech32(nsec).map_err(|err| err.to_string()) - }; + let secret_key = if nsec.starts_with("ncryptsec") { + let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap(); + encrypted_key + .to_secret_key(password) + .map_err(|err| err.to_string()) + } else { + SecretKey::from_bech32(nsec).map_err(|err| err.to_string()) + }; - match secret_key { - Ok(val) => { - let nostr_keys = Keys::new(val); - let npub = nostr_keys.public_key().to_bech32().unwrap(); - let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap(); + match secret_key { + Ok(val) => { + let nostr_keys = Keys::new(val); + let npub = nostr_keys.public_key().to_bech32().unwrap(); + let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap(); - let keyring = Entry::new(&npub, "nostr_secret").unwrap(); - let _ = keyring.set_password(&nsec); + let keyring = Entry::new(&npub, "nostr_secret").unwrap(); + let _ = keyring.set_password(&nsec); - let signer = NostrSigner::Keys(nostr_keys); - let client = &state.client; + let signer = NostrSigner::Keys(nostr_keys); + let client = &state.client; - // Update client's signer - client.set_signer(Some(signer)).await; + // Update client's signer + client.set_signer(Some(signer)).await; - Ok(npub) + Ok(npub) + } + Err(msg) => Err(msg), } - Err(msg) => Err(msg), - } } #[tauri::command] #[specta::specta] pub async fn connect_remote_account(uri: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; + let client = &state.client; - match NostrConnectURI::parse(uri) { - Ok(bunker_uri) => { - let app_keys = Keys::generate(); - let app_secret = app_keys.secret_key().unwrap().to_string(); + match NostrConnectURI::parse(uri) { + Ok(bunker_uri) => { + let app_keys = Keys::generate(); + let app_secret = app_keys.secret_key().unwrap().to_string(); - // Get remote user - let remote_user = bunker_uri.signer_public_key().unwrap(); - let remote_npub = remote_user.to_bech32().unwrap(); + // Get remote user + let remote_user = bunker_uri.signer_public_key().unwrap(); + let remote_npub = remote_user.to_bech32().unwrap(); - match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await { - Ok(signer) => { - let keyring = Entry::new(&remote_npub, "nostr_secret").unwrap(); - let _ = keyring.set_password(&app_secret); + match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await { + Ok(signer) => { + let keyring = Entry::new(&remote_npub, "nostr_secret").unwrap(); + let _ = keyring.set_password(&app_secret); - // Update signer - let _ = client.set_signer(Some(signer.into())).await; + // Update signer + let _ = client.set_signer(Some(signer.into())).await; - Ok(remote_npub) + Ok(remote_npub) + } + Err(err) => Err(err.to_string()), + } } Err(err) => Err(err.to_string()), - } } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn get_encrypted_key(npub: &str, password: &str) -> Result { - let keyring = Entry::new(npub, "nostr_secret").unwrap(); + let keyring = Entry::new(npub, "nostr_secret").unwrap(); - if let Ok(nsec) = keyring.get_password() { - let secret_key = SecretKey::from_bech32(nsec).unwrap(); - let new_key = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium); + if let Ok(nsec) = keyring.get_password() { + let secret_key = SecretKey::from_bech32(nsec).unwrap(); + let new_key = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium); - if let Ok(key) = new_key { - Ok(key.to_bech32().unwrap()) + if let Ok(key) = new_key { + Ok(key.to_bech32().unwrap()) + } else { + Err("Encrypt key failed".into()) + } } else { - Err("Encrypt key failed".into()) + Err("Key not found".into()) } - } else { - Err("Key not found".into()) - } } #[tauri::command] #[specta::specta] pub async fn load_account( - npub: &str, - bunker: Option<&str>, - state: State<'_, Nostr>, - app: tauri::AppHandle, + npub: &str, + bunker: Option<&str>, + state: State<'_, Nostr>, + app: tauri::AppHandle, ) -> Result { - let handle = app.clone(); - let client = &state.client; - let keyring = Entry::new(npub, "nostr_secret").unwrap(); - - let password = match keyring.get_password() { - Ok(pw) => pw, - Err(_) => return Err("Cancelled".into()), - }; - - match bunker { - Some(uri) => { - let app_keys = Keys::parse(password).expect("Secret Key is modified, please check again."); - - match NostrConnectURI::parse(uri) { - Ok(bunker_uri) => { - match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None).await { - Ok(signer) => client.set_signer(Some(signer.into())).await, - Err(err) => return Err(err.to_string()), - } - } - Err(err) => return Err(err.to_string()), - } - } - None => { - let keys = Keys::parse(password).expect("Secret Key is modified, please check again."); - let signer = NostrSigner::Keys(keys); - - // Update signer - client.set_signer(Some(signer)).await; - } - } - - // Connect to user's relay (NIP-65) - init_nip65(client).await; - - // Create tray (macOS) - #[cfg(target_os = "macos")] - create_tray_panel(npub, &handle); - - // Get user's contact list - if let Ok(contacts) = client.get_contact_list(None).await { - *state.contact_list.lock().unwrap() = contacts - }; - - // Get user's settings - if let Ok(settings) = get_user_settings(client).await { - *state.settings.lock().unwrap() = settings - }; - - tauri::async_runtime::spawn(async move { - let window = handle.get_window("main").unwrap(); - - let state = window.state::(); + let handle = app.clone(); let client = &state.client; - let contact_list = state.contact_list.lock().unwrap().clone(); + let keyring = Entry::new(npub, "nostr_secret").unwrap(); - let signer = client.signer().await.unwrap(); - let public_key = signer.public_key().await.unwrap(); - - if !contact_list.is_empty() { - let authors: Vec = contact_list.into_iter().map(|f| f.public_key).collect(); - - match client - .reconcile( - Filter::new() - .authors(authors) - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(NEWSFEED_NEG_LIMIT), - NegentropyOptions::default(), - ) - .await - { - Ok(_) => { - if handle.emit_to(EventTarget::Any, "synced", true).is_err() { - println!("Emit event failed.") - } - } - Err(_) => println!("Sync newsfeed failed."), - } + let password = match keyring.get_password() { + Ok(pw) => pw, + Err(_) => return Err("Cancelled".into()), }; - match client - .reconcile( - Filter::new() - .pubkey(public_key) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .limit(NOTIFICATION_NEG_LIMIT), - NegentropyOptions::default(), - ) - .await - { - Ok(_) => { - if handle.emit_to(EventTarget::Any, "synced", true).is_err() { - println!("Emit event failed.") - } - } - Err(_) => println!("Sync notification failed."), - }; + match bunker { + Some(uri) => { + let app_keys = + Keys::parse(password).expect("Secret Key is modified, please check again."); - let subscription_id = SubscriptionId::new("notification"); - let subscription = Filter::new() - .pubkey(public_key) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .since(Timestamp::now()); - - // Subscribing for new notification... - client - .subscribe_with_id(subscription_id, vec![subscription], None) - .await; - - // Handle notifications - client - .handle_notifications(|notification| async { - if let RelayPoolNotification::Message { message, .. } = notification { - if let RelayMessage::Event { - subscription_id, - event, - } = message - { - let id = subscription_id.to_string(); - - if id.starts_with("notification") { - if app - .emit_to( - EventTarget::window("panel"), - "notification", - event.as_json(), - ) - .is_err() - { - println!("Emit new notification failed.") - } - - let handle = app.app_handle(); - let author = client.metadata(event.pubkey).await.unwrap(); - - match event.kind() { - Kind::TextNote => { - if let Err(e) = handle - .notification() - .builder() - .body("Mentioned you in a thread.") - .title(author.display_name.unwrap_or_else(|| "Lume".to_string())) - .show() - { - println!("Failed to show notification: {:?}", e); - } + match NostrConnectURI::parse(uri) { + Ok(bunker_uri) => { + match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None) + .await + { + Ok(signer) => client.set_signer(Some(signer.into())).await, + Err(err) => return Err(err.to_string()), + } } - Kind::Repost => { - if let Err(e) = handle - .notification() - .builder() - .body("Reposted your note.") - .title(author.display_name.unwrap_or_else(|| "Lume".to_string())) - .show() - { - println!("Failed to show notification: {:?}", e); - } - } - Kind::Reaction => { - let content = event.content(); - if let Err(e) = handle - .notification() - .builder() - .body(content) - .title(author.display_name.unwrap_or_else(|| "Lume".to_string())) - .show() - { - println!("Failed to show notification: {:?}", e); - } - } - Kind::ZapReceipt => { - if let Err(e) = handle - .notification() - .builder() - .body("Zapped you.") - .title(author.display_name.unwrap_or_else(|| "Lume".to_string())) - .show() - { - println!("Failed to show notification: {:?}", e); - } - } - _ => {} - } - } else if id.starts_with("event-") { - let raw = event.as_json(); - let parsed = if event.kind == Kind::TextNote { - Some(parse_event(&event.content).await) - } else { - None - }; - - if app - .emit_to( - EventTarget::window(id), - "new_reply", - RichEvent { raw, parsed }, - ) - .is_err() - { - println!("Emit new notification failed.") - } - } else if id.starts_with("column-") { - let raw = event.as_json(); - let parsed = if event.kind == Kind::TextNote { - Some(parse_event(&event.content).await) - } else { - None - }; - - if app - .emit_to( - EventTarget::window(id), - "new_event", - RichEvent { raw, parsed }, - ) - .is_err() - { - println!("Emit new notification failed.") - } - } else { - println!("new event: {}", event.as_json()) + Err(err) => return Err(err.to_string()), } - } else { - println!("new message: {}", message.as_json()) - } } - Ok(false) - }) - .await - }); + None => { + let keys = Keys::parse(password).expect("Secret Key is modified, please check again."); + let signer = NostrSigner::Keys(keys); - Ok(true) + // Update signer + client.set_signer(Some(signer)).await; + } + } + + // Connect to user's relay (NIP-65) + init_nip65(client).await; + + // Get user's contact list + if let Ok(contacts) = client.get_contact_list(None).await { + *state.contact_list.lock().unwrap() = contacts + }; + + // Get user's settings + if let Ok(settings) = get_user_settings(client).await { + *state.settings.lock().unwrap() = settings + }; + + tauri::async_runtime::spawn(async move { + let window = handle.get_window("main").unwrap(); + + let state = window.state::(); + let client = &state.client; + let contact_list = state.contact_list.lock().unwrap().clone(); + + let signer = client.signer().await.unwrap(); + let public_key = signer.public_key().await.unwrap(); + + if !contact_list.is_empty() { + let authors: Vec = contact_list.into_iter().map(|f| f.public_key).collect(); + + match client + .reconcile( + Filter::new() + .authors(authors) + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(NEWSFEED_NEG_LIMIT), + NegentropyOptions::default(), + ) + .await + { + Ok(_) => { + if handle.emit_to(EventTarget::Any, "synced", true).is_err() { + println!("Emit event failed.") + } + } + Err(_) => println!("Sync newsfeed failed."), + } + }; + + match client + .reconcile( + Filter::new() + .pubkey(public_key) + .kinds(vec![ + Kind::TextNote, + Kind::Repost, + Kind::Reaction, + Kind::ZapReceipt, + ]) + .limit(NOTIFICATION_NEG_LIMIT), + NegentropyOptions::default(), + ) + .await + { + Ok(_) => { + if handle.emit_to(EventTarget::Any, "synced", true).is_err() { + println!("Emit event failed.") + } + } + Err(_) => println!("Sync notification failed."), + }; + + let subscription_id = SubscriptionId::new("notification"); + let subscription = Filter::new() + .pubkey(public_key) + .kinds(vec![ + Kind::TextNote, + Kind::Repost, + Kind::Reaction, + Kind::ZapReceipt, + ]) + .since(Timestamp::now()); + + // Subscribing for new notification... + let _ = client + .subscribe_with_id(subscription_id, vec![subscription], None) + .await; + + // Handle notifications + client + .handle_notifications(|notification| async { + if let RelayPoolNotification::Message { message, .. } = notification { + if let RelayMessage::Event { + subscription_id, + event, + } = message + { + let id = subscription_id.to_string(); + + if id.starts_with("notification") { + if app + .emit_to( + EventTarget::window("panel"), + "notification", + event.as_json(), + ) + .is_err() + { + println!("Emit new notification failed.") + } + + let handle = app.app_handle(); + let author = client.metadata(event.pubkey).await.unwrap(); + + match event.kind() { + Kind::TextNote => { + if let Err(e) = handle + .notification() + .builder() + .body("Mentioned you in a thread.") + .title( + author + .display_name + .unwrap_or_else(|| "Lume".to_string()), + ) + .show() + { + println!("Failed to show notification: {:?}", e); + } + } + Kind::Repost => { + if let Err(e) = handle + .notification() + .builder() + .body("Reposted your note.") + .title( + author + .display_name + .unwrap_or_else(|| "Lume".to_string()), + ) + .show() + { + println!("Failed to show notification: {:?}", e); + } + } + Kind::Reaction => { + let content = event.content(); + if let Err(e) = handle + .notification() + .builder() + .body(content) + .title( + author + .display_name + .unwrap_or_else(|| "Lume".to_string()), + ) + .show() + { + println!("Failed to show notification: {:?}", e); + } + } + Kind::ZapReceipt => { + if let Err(e) = handle + .notification() + .builder() + .body("Zapped you.") + .title( + author + .display_name + .unwrap_or_else(|| "Lume".to_string()), + ) + .show() + { + println!("Failed to show notification: {:?}", e); + } + } + _ => {} + } + } else if id.starts_with("event-") { + let raw = event.as_json(); + let parsed = if event.kind == Kind::TextNote { + Some(parse_event(&event.content).await) + } else { + None + }; + + if app + .emit_to( + EventTarget::window(id), + "new_reply", + RichEvent { raw, parsed }, + ) + .is_err() + { + println!("Emit new notification failed.") + } + } else if id.starts_with("column-") { + let raw = event.as_json(); + let parsed = if event.kind == Kind::TextNote { + Some(parse_event(&event.content).await) + } else { + None + }; + + if app + .emit_to( + EventTarget::window(id), + "new_event", + RichEvent { raw, parsed }, + ) + .is_err() + { + println!("Emit new notification failed.") + } + } else { + println!("new event: {}", event.as_json()) + } + } else { + println!("new message: {}", message.as_json()) + } + } + Ok(false) + }) + .await + }); + + Ok(true) } diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index 0aeb6f55..e1fe2558 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -11,584 +11,587 @@ use super::get_latest_event; #[tauri::command] #[specta::specta] pub async fn get_current_profile(state: State<'_, Nostr>) -> Result { - let client = &state.client; + let client = &state.client; - let signer = match client.signer().await { - Ok(signer) => signer, - Err(err) => return Err(format!("Failed to get signer: {}", err)), - }; + let signer = match client.signer().await { + Ok(signer) => signer, + Err(err) => return Err(format!("Failed to get signer: {}", err)), + }; - let public_key = match signer.public_key().await { - Ok(pk) => pk, - Err(err) => return Err(format!("Failed to get public key: {}", err)), - }; + let public_key = match signer.public_key().await { + Ok(pk) => pk, + Err(err) => return Err(format!("Failed to get public key: {}", err)), + }; - let filter = Filter::new() - .author(public_key) - .kind(Kind::Metadata) - .limit(1); + let filter = Filter::new() + .author(public_key) + .kind(Kind::Metadata) + .limit(1); - let events_result = client - .get_events_of(vec![filter], Some(Duration::from_secs(10))) - .await; + let events_result = client + .get_events_of(vec![filter], Some(Duration::from_secs(10))) + .await; - match events_result { - Ok(events) => { - if let Some(event) = get_latest_event(&events) { - match Metadata::from_json(&event.content) { - Ok(metadata) => Ok(metadata.as_json()), - Err(_) => Err("Failed to parse metadata.".into()), + match events_result { + Ok(events) => { + if let Some(event) = get_latest_event(&events) { + match Metadata::from_json(&event.content) { + Ok(metadata) => Ok(metadata.as_json()), + Err(_) => Err("Failed to parse metadata.".into()), + } + } else { + Err("No matching events found.".into()) + } } - } else { - Err("No matching events found.".into()) - } + Err(err) => Err(format!("Failed to get events: {}", err)), } - Err(err) => Err(format!("Failed to get events: {}", err)), - } } #[tauri::command] #[specta::specta] pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; - let public_key: Option = match Nip19::from_bech32(id) { - Ok(val) => match val { - Nip19::Pubkey(key) => Some(key), - Nip19::Profile(profile) => { - let relays = profile.relays; - for relay in relays.into_iter() { - let _ = client.add_relay(&relay).await.unwrap_or_default(); - client.connect_relay(&relay).await.unwrap_or_default(); - } - Some(profile.public_key) - } - _ => None, - }, - Err(_) => match PublicKey::from_str(id) { - Ok(val) => Some(val), - Err(_) => None, - }, - }; + let client = &state.client; + let public_key: Option = match Nip19::from_bech32(id) { + Ok(val) => match val { + Nip19::Pubkey(key) => Some(key), + Nip19::Profile(profile) => { + let relays = profile.relays; + for relay in relays.into_iter() { + let _ = client.add_relay(&relay).await.unwrap_or_default(); + client.connect_relay(&relay).await.unwrap_or_default(); + } + Some(profile.public_key) + } + _ => None, + }, + Err(_) => match PublicKey::from_str(id) { + Ok(val) => Some(val), + Err(_) => None, + }, + }; - if let Some(author) = public_key { - let filter = Filter::new().author(author).kind(Kind::Metadata).limit(1); - let query = client - .get_events_of(vec![filter], Some(Duration::from_secs(10))) - .await; + if let Some(author) = public_key { + let filter = Filter::new().author(author).kind(Kind::Metadata).limit(1); + let query = client + .get_events_of(vec![filter], Some(Duration::from_secs(10))) + .await; - if let Ok(events) = query { - if let Some(event) = events.first() { - if let Ok(metadata) = Metadata::from_json(&event.content) { - Ok(metadata.as_json()) + if let Ok(events) = query { + if let Some(event) = events.first() { + if let Ok(metadata) = Metadata::from_json(&event.content) { + Ok(metadata.as_json()) + } else { + Err("Parse metadata failed".into()) + } + } else { + let rand_metadata = Metadata::new(); + Ok(rand_metadata.as_json()) + } } else { - Err("Parse metadata failed".into()) + Err("Get metadata failed".into()) } - } else { - let rand_metadata = Metadata::new(); - Ok(rand_metadata.as_json()) - } } else { - Err("Get metadata failed".into()) + Err("Public Key is not valid".into()) } - } else { - Err("Public Key is not valid".into()) - } } #[tauri::command] #[specta::specta] pub async fn set_contact_list( - public_keys: Vec<&str>, - state: State<'_, Nostr>, + public_keys: Vec<&str>, + state: State<'_, Nostr>, ) -> Result { - let client = &state.client; - let contact_list: Vec = public_keys - .into_iter() - .filter_map(|p| match PublicKey::from_hex(p) { - Ok(pk) => Some(Contact::new(pk, None, Some(""))), - Err(_) => None, - }) - .collect(); + let client = &state.client; + let contact_list: Vec = public_keys + .into_iter() + .filter_map(|p| match PublicKey::from_hex(p) { + Ok(pk) => Some(Contact::new(pk, None, Some(""))), + Err(_) => None, + }) + .collect(); - match client.set_contact_list(contact_list).await { - Ok(_) => Ok(true), - Err(err) => Err(err.to_string()), - } + match client.set_contact_list(contact_list).await { + Ok(_) => Ok(true), + Err(err) => Err(err.to_string()), + } } #[tauri::command] #[specta::specta] pub async fn get_contact_list(state: State<'_, Nostr>) -> Result, String> { - let client = &state.client; + let client = &state.client; - match client.get_contact_list(Some(Duration::from_secs(10))).await { - Ok(contact_list) => { - if !contact_list.is_empty() { - let list = contact_list - .into_iter() - .map(|f| f.public_key.to_hex()) - .collect(); + match client.get_contact_list(Some(Duration::from_secs(10))).await { + Ok(contact_list) => { + if !contact_list.is_empty() { + let list = contact_list + .into_iter() + .map(|f| f.public_key.to_hex()) + .collect(); - Ok(list) - } else { - Err("Empty.".into()) - } + Ok(list) + } else { + Err("Empty.".into()) + } + } + Err(err) => Err(err.to_string()), } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn create_profile( - name: &str, - display_name: &str, - about: &str, - picture: &str, - banner: &str, - nip05: &str, - lud16: &str, - website: &str, - state: State<'_, Nostr>, + name: &str, + display_name: &str, + about: &str, + picture: &str, + banner: &str, + nip05: &str, + lud16: &str, + website: &str, + state: State<'_, Nostr>, ) -> Result { - let client = &state.client; - let mut metadata = Metadata::new() - .name(name) - .display_name(display_name) - .about(about) - .nip05(nip05) - .lud16(lud16); + let client = &state.client; + let mut metadata = Metadata::new() + .name(name) + .display_name(display_name) + .about(about) + .nip05(nip05) + .lud16(lud16); - if let Ok(url) = Url::parse(picture) { - metadata = metadata.picture(url) - } + if let Ok(url) = Url::parse(picture) { + metadata = metadata.picture(url) + } - if let Ok(url) = Url::parse(banner) { - metadata = metadata.banner(url) - } + if let Ok(url) = Url::parse(banner) { + metadata = metadata.banner(url) + } - if let Ok(url) = Url::parse(website) { - metadata = metadata.website(url) - } + if let Ok(url) = Url::parse(website) { + metadata = metadata.website(url) + } - if let Ok(event_id) = client.set_metadata(&metadata).await { - Ok(event_id.to_string()) - } else { - Err("Create profile failed".into()) - } + if let Ok(event_id) = client.set_metadata(&metadata).await { + Ok(event_id.to_string()) + } else { + Err("Create profile failed".into()) + } } #[tauri::command] #[specta::specta] pub async fn is_contact_list_empty(state: State<'_, Nostr>) -> Result { - let contact_list = state.contact_list.lock().unwrap(); - Ok(contact_list.is_empty()) + let contact_list = state.contact_list.lock().unwrap(); + Ok(contact_list.is_empty()) } #[tauri::command] #[specta::specta] pub async fn check_contact(hex: &str, state: State<'_, Nostr>) -> Result { - let contact_list = state.contact_list.lock().unwrap(); + let contact_list = state.contact_list.lock().unwrap(); - match PublicKey::from_str(hex) { - Ok(public_key) => match contact_list.iter().position(|x| x.public_key == public_key) { - Some(_) => Ok(true), - None => Ok(false), - }, - Err(err) => Err(err.to_string()), - } + match PublicKey::from_str(hex) { + Ok(public_key) => match contact_list.iter().position(|x| x.public_key == public_key) { + Some(_) => Ok(true), + None => Ok(false), + }, + Err(err) => Err(err.to_string()), + } } #[tauri::command] #[specta::specta] pub async fn toggle_contact( - hex: &str, - alias: Option<&str>, - state: State<'_, Nostr>, + hex: &str, + alias: Option<&str>, + state: State<'_, Nostr>, ) -> Result { - let client = &state.client; + let client = &state.client; - match client.get_contact_list(None).await { - Ok(mut contact_list) => { - let public_key = PublicKey::from_str(hex).unwrap(); + match client.get_contact_list(None).await { + Ok(mut contact_list) => { + let public_key = PublicKey::from_str(hex).unwrap(); - match contact_list.iter().position(|x| x.public_key == public_key) { - Some(index) => { - // Remove contact - contact_list.remove(index); + match contact_list.iter().position(|x| x.public_key == public_key) { + Some(index) => { + // Remove contact + contact_list.remove(index); + } + None => { + // TODO: Add relay_url + let new_contact = Contact::new(public_key, None, alias); + // Add new contact + contact_list.push(new_contact); + } + } + + // Update local state + state.contact_list.lock().unwrap().clone_from(&contact_list); + + // Publish + match client.set_contact_list(contact_list).await { + Ok(event_id) => Ok(event_id.to_string()), + Err(err) => Err(err.to_string()), + } } - None => { - // TODO: Add relay_url - let new_contact = Contact::new(public_key, None, alias); - // Add new contact - contact_list.push(new_contact); - } - } - - // Update local state - state.contact_list.lock().unwrap().clone_from(&contact_list); - - // Publish - match client.set_contact_list(contact_list).await { - Ok(event_id) => Ok(event_id.to_string()), Err(err) => Err(err.to_string()), - } } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn set_nstore( - key: &str, - content: &str, - state: State<'_, Nostr>, + key: &str, + content: &str, + state: State<'_, Nostr>, ) -> Result { - let client = &state.client; + let client = &state.client; - match client.signer().await { - Ok(signer) => { - let public_key = match signer.public_key().await { - Ok(pk) => pk, - Err(err) => return Err(format!("Failed to get public key: {}", err)), - }; + match client.signer().await { + Ok(signer) => { + let public_key = match signer.public_key().await { + Ok(pk) => pk, + Err(err) => return Err(format!("Failed to get public key: {}", err)), + }; - let encrypted = match signer.nip44_encrypt(public_key, content).await { - Ok(enc) => enc, - Err(err) => return Err(format!("Encryption failed: {}", err)), - }; + let encrypted = match signer.nip44_encrypt(public_key, content).await { + Ok(enc) => enc, + Err(err) => return Err(format!("Encryption failed: {}", err)), + }; - let tag = Tag::identifier(key); - let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]); + let tag = Tag::identifier(key); + let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]); - match client.send_event_builder(builder).await { - Ok(event_id) => Ok(event_id.to_string()), + match client.send_event_builder(builder).await { + Ok(event_id) => Ok(event_id.to_string()), + Err(err) => Err(err.to_string()), + } + } Err(err) => Err(err.to_string()), - } } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; + let client = &state.client; - if let Ok(signer) = client.signer().await { - let public_key = match signer.public_key().await { - Ok(pk) => pk, - Err(err) => return Err(format!("Failed to get public key: {}", err)), - }; + if let Ok(signer) = client.signer().await { + let public_key = match signer.public_key().await { + Ok(pk) => pk, + Err(err) => return Err(format!("Failed to get public key: {}", err)), + }; - let filter = Filter::new() - .author(public_key) - .kind(Kind::ApplicationSpecificData) - .identifier(key) - .limit(1); + let filter = Filter::new() + .author(public_key) + .kind(Kind::ApplicationSpecificData) + .identifier(key) + .limit(1); - match client - .get_events_of(vec![filter], Some(Duration::from_secs(5))) - .await - { - Ok(events) => { - if let Some(event) = events.first() { - let content = event.content(); - match signer.nip44_decrypt(public_key, content).await { - Ok(decrypted) => Ok(decrypted), - Err(_) => Err(event.content.to_string()), - } - } else { - Err("Value not found".into()) + match client + .get_events_of(vec![filter], Some(Duration::from_secs(5))) + .await + { + Ok(events) => { + if let Some(event) = events.first() { + let content = event.content(); + match signer.nip44_decrypt(public_key, content).await { + Ok(decrypted) => Ok(decrypted), + Err(_) => Err(event.content.to_string()), + } + } else { + Err("Value not found".into()) + } + } + Err(err) => Err(err.to_string()), } - } - Err(err) => Err(err.to_string()), + } else { + Err("Signer is required".into()) } - } else { - Err("Signer is required".into()) - } } #[tauri::command] #[specta::specta] pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; + let client = &state.client; - if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) { - let nwc = NWC::new(nwc_uri); - let keyring = Entry::new("Lume Secret", "Bitcoin Connect").map_err(|e| e.to_string())?; - keyring.set_password(uri).map_err(|e| e.to_string())?; - client.set_zapper(nwc).await; + if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) { + let nwc = NWC::new(nwc_uri); + let keyring = Entry::new("Lume Secret", "Bitcoin Connect").map_err(|e| e.to_string())?; + keyring.set_password(uri).map_err(|e| e.to_string())?; + client.set_zapper(nwc).await; - Ok(true) - } else { - Err("Set NWC failed".into()) - } + Ok(true) + } else { + Err("Set NWC failed".into()) + } } #[tauri::command] #[specta::specta] pub async fn load_wallet(state: State<'_, Nostr>) -> Result { - let client = &state.client; - let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap(); + let client = &state.client; + let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap(); - match keyring.get_password() { - Ok(val) => { - let uri = NostrWalletConnectURI::from_str(&val).unwrap(); - let nwc = NWC::new(uri); + match keyring.get_password() { + Ok(val) => { + let uri = NostrWalletConnectURI::from_str(&val).unwrap(); + let nwc = NWC::new(uri); - // Get current balance - let balance = nwc.get_balance().await; + // Get current balance + let balance = nwc.get_balance().await; - // Update zapper - client.set_zapper(nwc).await; + // Update zapper + client.set_zapper(nwc).await; - match balance { - Ok(val) => Ok(val.to_string()), - Err(_) => Err("Get balance failed.".into()), - } + match balance { + Ok(val) => Ok(val.to_string()), + Err(_) => Err("Get balance failed.".into()), + } + } + Err(_) => Err("NWC not found.".into()), } - Err(_) => Err("NWC not found.".into()), - } } #[tauri::command] #[specta::specta] pub async fn remove_wallet(state: State<'_, Nostr>) -> Result<(), ()> { - let client = &state.client; - let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap(); + let client = &state.client; + let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap(); - match keyring.delete_password() { - Ok(_) => { - client.unset_zapper().await; - Ok(()) + match keyring.delete_password() { + Ok(_) => { + client.unset_zapper().await; + Ok(()) + } + Err(_) => Err(()), } - Err(_) => Err(()), - } } #[tauri::command] #[specta::specta] pub async fn zap_profile( - id: &str, - amount: &str, - message: &str, - state: State<'_, Nostr>, + id: &str, + amount: &str, + message: &str, + state: State<'_, Nostr>, ) -> Result { - let client = &state.client; - let public_key = match Nip19::from_bech32(id) { - Ok(val) => match val { - Nip19::Pubkey(key) => key, - Nip19::Profile(profile) => profile.public_key, - _ => return Err("Public Key is not valid.".into()), - }, - Err(_) => match PublicKey::from_str(id) { - Ok(val) => val, - Err(_) => return Err("Public Key is not valid.".into()), - }, - }; + let client = &state.client; + let public_key = match Nip19::from_bech32(id) { + Ok(val) => match val { + Nip19::Pubkey(key) => key, + Nip19::Profile(profile) => profile.public_key, + _ => return Err("Public Key is not valid.".into()), + }, + Err(_) => match PublicKey::from_str(id) { + Ok(val) => val, + Err(_) => return Err("Public Key is not valid.".into()), + }, + }; - let details = ZapDetails::new(ZapType::Private).message(message); - let num = match amount.parse::() { - Ok(val) => val, - Err(_) => return Err("Invalid amount.".into()), - }; + let details = ZapDetails::new(ZapType::Private).message(message); + let num = match amount.parse::() { + Ok(val) => val, + Err(_) => return Err("Invalid amount.".into()), + }; - if client.zap(public_key, num, Some(details)).await.is_ok() { - Ok(true) - } else { - Err("Zap profile failed".into()) - } + if client.zap(public_key, num, Some(details)).await.is_ok() { + Ok(true) + } else { + Err("Zap profile failed".into()) + } } #[tauri::command] #[specta::specta] pub async fn zap_event( - id: &str, - amount: &str, - message: &str, - state: State<'_, Nostr>, + id: &str, + amount: &str, + message: &str, + state: State<'_, Nostr>, ) -> Result { - 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 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 num = match amount.parse::() { - Ok(val) => val, - Err(_) => return Err("Invalid amount.".into()), - }; + let details = ZapDetails::new(ZapType::Private).message(message); + let num = match amount.parse::() { + Ok(val) => val, + Err(_) => return Err("Invalid amount.".into()), + }; - if client.zap(event_id, num, Some(details)).await.is_ok() { - Ok(true) - } else { - Err("Zap event failed".into()) - } + if client.zap(event_id, num, Some(details)).await.is_ok() { + Ok(true) + } else { + Err("Zap event failed".into()) + } } #[tauri::command] #[specta::specta] pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; + let client = &state.client; - match PublicKey::from_bech32(npub) { - Ok(author) => { - let mut contact_list: Vec = Vec::new(); - let contact_list_filter = Filter::new() - .author(author) - .kind(Kind::ContactList) - .limit(1); + match PublicKey::from_bech32(npub) { + Ok(author) => { + let mut contact_list: Vec = Vec::new(); + let contact_list_filter = Filter::new() + .author(author) + .kind(Kind::ContactList) + .limit(1); - if let Ok(contact_list_events) = client.get_events_of(vec![contact_list_filter], None).await { - for event in contact_list_events.into_iter() { - for tag in event.into_iter_tags() { - if let Some(TagStandard::PublicKey { - public_key, - relay_url, - alias, - uppercase: false, - }) = tag.to_standardized() + if let Ok(contact_list_events) = + client.get_events_of(vec![contact_list_filter], None).await { - contact_list.push(Contact::new(public_key, relay_url, alias)) + for event in contact_list_events.into_iter() { + for tag in event.into_iter_tags() { + if let Some(TagStandard::PublicKey { + public_key, + relay_url, + alias, + uppercase: false, + }) = tag.to_standardized() + { + contact_list.push(Contact::new(public_key, relay_url, alias)) + } + } + } } - } - } - } - match client.set_contact_list(contact_list).await { - Ok(_) => Ok(true), + match client.set_contact_list(contact_list).await { + Ok(_) => Ok(true), + Err(err) => Err(err.to_string()), + } + } Err(err) => Err(err.to_string()), - } } - Err(err) => Err(err.to_string()), - } } pub async fn get_following( - state: State<'_, Nostr>, - public_key: &str, - timeout: Option, + state: State<'_, Nostr>, + public_key: &str, + timeout: Option, ) -> Result, String> { - let client = &state.client; - let public_key = match PublicKey::from_str(public_key) { - Ok(val) => val, - Err(err) => return Err(err.to_string()), - }; - let duration = timeout.map(Duration::from_secs); - let filter = Filter::new().kind(Kind::ContactList).author(public_key); - let events = match client.get_events_of(vec![filter], duration).await { - Ok(events) => events, - Err(err) => return Err(err.to_string()), - }; - let mut ret: Vec = vec![]; - if let Some(latest_event) = events.iter().max_by_key(|event| event.created_at()) { - ret.extend(latest_event.tags().iter().filter_map(|tag| { - if let Some(TagStandard::PublicKey { - uppercase: false, .. - }) = ::clone(tag).to_standardized() - { - tag.content().map(String::from) - } else { - None - } - })); - } - Ok(ret) + let client = &state.client; + let public_key = match PublicKey::from_str(public_key) { + Ok(val) => val, + Err(err) => return Err(err.to_string()), + }; + let duration = timeout.map(Duration::from_secs); + let filter = Filter::new().kind(Kind::ContactList).author(public_key); + let events = match client.get_events_of(vec![filter], duration).await { + Ok(events) => events, + Err(err) => return Err(err.to_string()), + }; + let mut ret: Vec = vec![]; + if let Some(latest_event) = events.iter().max_by_key(|event| event.created_at()) { + ret.extend(latest_event.tags().iter().filter_map(|tag| { + if let Some(TagStandard::PublicKey { + uppercase: false, .. + }) = ::clone(tag).to_standardized() + { + tag.content().map(String::from) + } else { + None + } + })); + } + Ok(ret) } pub async fn get_followers( - state: State<'_, Nostr>, - public_key: &str, - timeout: Option, + state: State<'_, Nostr>, + public_key: &str, + timeout: Option, ) -> Result, String> { - let client = &state.client; - let public_key = match PublicKey::from_str(public_key) { - Ok(val) => val, - Err(err) => return Err(err.to_string()), - }; - let duration = timeout.map(Duration::from_secs); + let client = &state.client; + let public_key = match PublicKey::from_str(public_key) { + Ok(val) => val, + Err(err) => return Err(err.to_string()), + }; + let duration = timeout.map(Duration::from_secs); - let filter = Filter::new().kind(Kind::ContactList).custom_tag( - SingleLetterTag::lowercase(Alphabet::P), - vec![public_key.to_hex()], - ); - let events = match client.get_events_of(vec![filter], duration).await { - Ok(events) => events, - Err(err) => return Err(err.to_string()), - }; - let ret: Vec = events - .into_iter() - .map(|event| event.author().to_hex()) - .collect(); - Ok(ret) - // TODO: get more than 500 events + let filter = Filter::new().kind(Kind::ContactList).custom_tag( + SingleLetterTag::lowercase(Alphabet::P), + vec![public_key.to_hex()], + ); + let events = match client.get_events_of(vec![filter], duration).await { + Ok(events) => events, + Err(err) => return Err(err.to_string()), + }; + let ret: Vec = events + .into_iter() + .map(|event| event.author().to_hex()) + .collect(); + Ok(ret) + // TODO: get more than 500 events } #[tauri::command] #[specta::specta] pub async fn get_notifications(state: State<'_, Nostr>) -> Result, String> { - let client = &state.client; + let client = &state.client; - match client.signer().await { - Ok(signer) => { - let public_key = signer.public_key().await.unwrap(); - let filter = Filter::new() - .pubkey(public_key) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .limit(200); + match client.signer().await { + Ok(signer) => { + let public_key = signer.public_key().await.unwrap(); + let filter = Filter::new() + .pubkey(public_key) + .kinds(vec![ + Kind::TextNote, + Kind::Repost, + Kind::Reaction, + Kind::ZapReceipt, + ]) + .limit(200); - match client - .database() - .query(vec![filter], Order::default()) - .await - { - Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), + match client + .database() + .query(vec![filter], Order::default()) + .await + { + Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), + Err(err) => Err(err.to_string()), + } + } Err(err) => Err(err.to_string()), - } } - Err(err) => Err(err.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn get_settings(state: State<'_, Nostr>) -> Result { - let settings = state.settings.lock().unwrap().clone(); - Ok(settings) + let settings = state.settings.lock().unwrap().clone(); + Ok(settings) } #[tauri::command] #[specta::specta] pub async fn set_new_settings(settings: &str, state: State<'_, Nostr>) -> Result<(), ()> { - let parsed: Settings = serde_json::from_str(settings).expect("Could not parse settings payload"); - *state.settings.lock().unwrap() = parsed; + let parsed: Settings = + serde_json::from_str(settings).expect("Could not parse settings payload"); + *state.settings.lock().unwrap() = parsed; - Ok(()) + Ok(()) } #[tauri::command] #[specta::specta] pub async fn verify_nip05(key: &str, nip05: &str) -> Result { - match PublicKey::from_str(key) { - Ok(public_key) => { - let status = nip05::verify(&public_key, nip05, None).await; - Ok(status.is_ok()) + match PublicKey::from_str(key) { + Ok(public_key) => { + let status = nip05::verify(&public_key, nip05, None).await; + Ok(status.is_ok()) + } + Err(err) => Err(err.to_string()), } - Err(err) => Err(err.to_string()), - } } diff --git a/src-tauri/src/nostr/relay.rs b/src-tauri/src/nostr/relay.rs index 57948286..d5f36f71 100644 --- a/src-tauri/src/nostr/relay.rs +++ b/src-tauri/src/nostr/relay.rs @@ -3,155 +3,155 @@ use nostr_sdk::prelude::*; use serde::Serialize; use specta::Type; use std::{ - fs::OpenOptions, - io::{self, BufRead, Write}, + fs::OpenOptions, + io::{self, BufRead, Write}, }; use tauri::{path::BaseDirectory, Manager, State}; #[derive(Serialize, Type)] pub struct Relays { - connected: Vec, - read: Option>, - write: Option>, - both: Option>, + connected: Vec, + read: Option>, + write: Option>, + both: Option>, } #[tauri::command] #[specta::specta] pub async fn get_relays(state: State<'_, Nostr>) -> Result { - let client = &state.client; + let client = &state.client; - let connected_relays = client - .relays() - .await - .into_keys() - .map(|url| url.to_string()) - .collect::>(); + let connected_relays = client + .relays() + .await + .into_keys() + .map(|url| url.to_string()) + .collect::>(); - let signer = client.signer().await.map_err(|e| e.to_string())?; - let public_key = signer.public_key().await.map_err(|e| e.to_string())?; + let signer = client.signer().await.map_err(|e| e.to_string())?; + let public_key = signer.public_key().await.map_err(|e| e.to_string())?; - let filter = Filter::new() - .author(public_key) - .kind(Kind::RelayList) - .limit(1); + let filter = Filter::new() + .author(public_key) + .kind(Kind::RelayList) + .limit(1); - match client.get_events_of(vec![filter], None).await { - Ok(events) => { - if let Some(event) = events.first() { - let nip65_list = nip65::extract_relay_list(event).collect::>(); + match client.get_events_of(vec![filter], None).await { + Ok(events) => { + if let Some(event) = events.first() { + let nip65_list = nip65::extract_relay_list(event).collect::>(); - let read = nip65_list - .iter() - .filter_map(|(url, meta)| { - if let Some(RelayMetadata::Read) = meta { - Some(url.to_string()) + let read = nip65_list + .iter() + .filter_map(|(url, meta)| { + if let Some(RelayMetadata::Read) = meta { + Some(url.to_string()) + } else { + None + } + }) + .collect(); + + let write = nip65_list + .iter() + .filter_map(|(url, meta)| { + if let Some(RelayMetadata::Write) = meta { + Some(url.to_string()) + } else { + None + } + }) + .collect(); + + let both = nip65_list + .iter() + .filter_map(|(url, meta)| { + if meta.is_none() { + Some(url.to_string()) + } else { + None + } + }) + .collect(); + + Ok(Relays { + connected: connected_relays, + read: Some(read), + write: Some(write), + both: Some(both), + }) } else { - None + Ok(Relays { + connected: connected_relays, + read: None, + write: None, + both: None, + }) } - }) - .collect(); - - let write = nip65_list - .iter() - .filter_map(|(url, meta)| { - if let Some(RelayMetadata::Write) = meta { - Some(url.to_string()) - } else { - None - } - }) - .collect(); - - let both = nip65_list - .iter() - .filter_map(|(url, meta)| { - if meta.is_none() { - Some(url.to_string()) - } else { - None - } - }) - .collect(); - - Ok(Relays { - connected: connected_relays, - read: Some(read), - write: Some(write), - both: Some(both), - }) - } else { - Ok(Relays { - connected: connected_relays, - read: None, - write: None, - both: None, - }) - } + } + Err(e) => Err(e.to_string()), } - Err(e) => Err(e.to_string()), - } } #[tauri::command] #[specta::specta] pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; - let status = client.add_relay(relay).await.map_err(|e| e.to_string())?; - if status { - println!("Connecting to relay: {}", relay); - client - .connect_relay(relay) - .await - .map_err(|e| e.to_string())?; - } - Ok(status) + let client = &state.client; + let status = client.add_relay(relay).await.map_err(|e| e.to_string())?; + if status { + println!("Connecting to relay: {}", relay); + client + .connect_relay(relay) + .await + .map_err(|e| e.to_string())?; + } + Ok(status) } #[tauri::command] #[specta::specta] pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result { - let client = &state.client; - client - .remove_relay(relay) - .await - .map_err(|e| e.to_string())?; - client - .disconnect_relay(relay) - .await - .map_err(|e| e.to_string())?; - Ok(true) + let client = &state.client; + client + .remove_relay(relay) + .await + .map_err(|e| e.to_string())?; + client + .disconnect_relay(relay) + .await + .map_err(|e| e.to_string())?; + Ok(true) } #[tauri::command] #[specta::specta] pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result, String> { - let relays_path = app - .path() - .resolve("resources/relays.txt", BaseDirectory::Resource) - .map_err(|e| e.to_string())?; + let relays_path = app + .path() + .resolve("resources/relays.txt", BaseDirectory::Resource) + .map_err(|e| e.to_string())?; - let file = std::fs::File::open(relays_path).map_err(|e| e.to_string())?; - let reader = io::BufReader::new(file); + let file = std::fs::File::open(relays_path).map_err(|e| e.to_string())?; + let reader = io::BufReader::new(file); - reader - .lines() - .collect::, io::Error>>() - .map_err(|e| e.to_string()) + reader + .lines() + .collect::, io::Error>>() + .map_err(|e| e.to_string()) } #[tauri::command] #[specta::specta] pub fn save_bootstrap_relays(relays: &str, app: tauri::AppHandle) -> Result<(), String> { - let relays_path = app - .path() - .resolve("resources/relays.txt", BaseDirectory::Resource) - .map_err(|e| e.to_string())?; + let relays_path = app + .path() + .resolve("resources/relays.txt", BaseDirectory::Resource) + .map_err(|e| e.to_string())?; - let mut file = OpenOptions::new() - .write(true) - .open(relays_path) - .map_err(|e| e.to_string())?; + let mut file = OpenOptions::new() + .write(true) + .open(relays_path) + .map_err(|e| e.to_string())?; - file.write_all(relays.as_bytes()).map_err(|e| e.to_string()) + file.write_all(relays.as_bytes()).map_err(|e| e.to_string()) } diff --git a/src-tauri/src/nostr/utils.rs b/src-tauri/src/nostr/utils.rs index 151d91ac..57a13718 100644 --- a/src-tauri/src/nostr/utils.rs +++ b/src-tauri/src/nostr/utils.rs @@ -11,255 +11,255 @@ use url::Url; #[derive(Debug, Clone, Serialize, Type)] pub struct Meta { - pub content: String, - pub images: Vec, - pub videos: Vec, - pub events: Vec, - pub mentions: Vec, - pub hashtags: Vec, + pub content: String, + pub images: Vec, + pub videos: Vec, + pub events: Vec, + pub mentions: Vec, + pub hashtags: Vec, } const NOSTR_EVENTS: [&str; 10] = [ - "@nevent1", - "@note1", - "@nostr:note1", - "@nostr:nevent1", - "nostr:note1", - "note1", - "nostr:nevent1", - "nevent1", - "Nostr:note1", - "Nostr:nevent1", + "@nevent1", + "@note1", + "@nostr:note1", + "@nostr:nevent1", + "nostr:note1", + "note1", + "nostr:nevent1", + "nevent1", + "Nostr:note1", + "Nostr:nevent1", ]; const NOSTR_MENTIONS: [&str; 10] = [ - "@npub1", - "nostr:npub1", - "nostr:nprofile1", - "nostr:naddr1", - "npub1", - "nprofile1", - "naddr1", - "Nostr:npub1", - "Nostr:nprofile1", - "Nostr:naddr1", + "@npub1", + "nostr:npub1", + "nostr:nprofile1", + "nostr:naddr1", + "npub1", + "nprofile1", + "naddr1", + "Nostr:npub1", + "Nostr:nprofile1", + "Nostr:naddr1", ]; const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"]; const VIDEOS: [&str; 5] = ["mp4", "mov", "avi", "webm", "mkv"]; pub fn get_latest_event(events: &[Event]) -> Option<&Event> { - events.iter().next() + events.iter().next() } pub fn dedup_event(events: &[Event]) -> Vec { - let mut seen_ids = HashSet::new(); - events - .iter() - .filter(|&event| { - let e = TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)); - let e_tags: Vec<&Tag> = event.tags.iter().filter(|el| el.kind() == e).collect(); - let ids: Vec<&str> = e_tags.iter().filter_map(|tag| tag.content()).collect(); - let is_dup = ids.iter().any(|id| seen_ids.contains(*id)); + let mut seen_ids = HashSet::new(); + events + .iter() + .filter(|&event| { + let e = TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)); + let e_tags: Vec<&Tag> = event.tags.iter().filter(|el| el.kind() == e).collect(); + let ids: Vec<&str> = e_tags.iter().filter_map(|tag| tag.content()).collect(); + let is_dup = ids.iter().any(|id| seen_ids.contains(*id)); - for id in &ids { - seen_ids.insert(*id); - } + for id in &ids { + seen_ids.insert(*id); + } - !is_dup - }) - .cloned() - .collect() + !is_dup + }) + .cloned() + .collect() } pub async fn parse_event(content: &str) -> Meta { - let mut finder = LinkFinder::new(); - finder.url_must_have_scheme(false); + let mut finder = LinkFinder::new(); + finder.url_must_have_scheme(false); - // Get urls - let urls: Vec<_> = finder.links(content).collect(); - // Get words - let words: Vec<_> = content.split_whitespace().collect(); + // Get urls + let urls: Vec<_> = finder.links(content).collect(); + // Get words + let words: Vec<_> = content.split_whitespace().collect(); - let hashtags = words - .iter() - .filter(|&&word| word.starts_with('#')) - .map(|&s| s.to_string()) - .collect::>(); + let hashtags = words + .iter() + .filter(|&&word| word.starts_with('#')) + .map(|&s| s.to_string()) + .collect::>(); - let events = words - .iter() - .filter(|&&word| NOSTR_EVENTS.iter().any(|&el| word.starts_with(el))) - .map(|&s| s.to_string()) - .collect::>(); + let events = words + .iter() + .filter(|&&word| NOSTR_EVENTS.iter().any(|&el| word.starts_with(el))) + .map(|&s| s.to_string()) + .collect::>(); - let mentions = words - .iter() - .filter(|&&word| NOSTR_MENTIONS.iter().any(|&el| word.starts_with(el))) - .map(|&s| s.to_string()) - .collect::>(); + let mentions = words + .iter() + .filter(|&&word| NOSTR_MENTIONS.iter().any(|&el| word.starts_with(el))) + .map(|&s| s.to_string()) + .collect::>(); - let mut images = Vec::new(); - let mut videos = Vec::new(); - let mut text = content.to_string(); + let mut images = Vec::new(); + let mut videos = Vec::new(); + let mut text = content.to_string(); - if !urls.is_empty() { - let client = Client::new(); + if !urls.is_empty() { + let client = Client::new(); - for url in urls { - let url_str = url.as_str(); + for url in urls { + let url_str = url.as_str(); - if let Ok(parsed_url) = Url::from_str(url_str) { - if let Some(ext) = parsed_url - .path_segments() - .and_then(|segments| segments.last().and_then(|s| s.split('.').last())) - { - if IMAGES.contains(&ext) { - text = text.replace(url_str, ""); - images.push(url_str.to_string()); - // Process the next item. - continue; - } - if VIDEOS.contains(&ext) { - text = text.replace(url_str, ""); - videos.push(url_str.to_string()); - // Process the next item. - continue; - } - } + if let Ok(parsed_url) = Url::from_str(url_str) { + if let Some(ext) = parsed_url + .path_segments() + .and_then(|segments| segments.last().and_then(|s| s.split('.').last())) + { + if IMAGES.contains(&ext) { + text = text.replace(url_str, ""); + images.push(url_str.to_string()); + // Process the next item. + continue; + } + if VIDEOS.contains(&ext) { + text = text.replace(url_str, ""); + videos.push(url_str.to_string()); + // Process the next item. + continue; + } + } - // Check the content type of URL via HEAD request - if let Ok(res) = client.head(url_str).send().await { - if let Some(content_type) = res.headers().get("Content-Type") { - if content_type.to_str().unwrap_or("").starts_with("image") { - text = text.replace(url_str, ""); - images.push(url_str.to_string()); - // Process the next item. - continue; + // Check the content type of URL via HEAD request + if let Ok(res) = client.head(url_str).send().await { + if let Some(content_type) = res.headers().get("Content-Type") { + if content_type.to_str().unwrap_or("").starts_with("image") { + text = text.replace(url_str, ""); + images.push(url_str.to_string()); + // Process the next item. + continue; + } + } + } } - } } - } } - } - // Clean up the resulting content string to remove extra spaces - let cleaned_text = text.trim().to_string(); + // Clean up the resulting content string to remove extra spaces + let cleaned_text = text.trim().to_string(); - Meta { - content: cleaned_text, - events, - mentions, - hashtags, - images, - videos, - } + Meta { + content: cleaned_text, + events, + mentions, + hashtags, + images, + videos, + } } pub fn create_event_tags(content: &str) -> Vec { - let mut tags: Vec = vec![]; - let mut tag_set: HashSet = HashSet::new(); + let mut tags: Vec = vec![]; + let mut tag_set: HashSet = HashSet::new(); - // Get words - let words: Vec<_> = content.split_whitespace().collect(); + // Get words + let words: Vec<_> = content.split_whitespace().collect(); - // Get mentions - let mentions = words - .iter() - .filter(|&&word| ["nostr:", "@"].iter().any(|&el| word.starts_with(el))) - .map(|&s| s.to_string()) - .collect::>(); + // Get mentions + let mentions = words + .iter() + .filter(|&&word| ["nostr:", "@"].iter().any(|&el| word.starts_with(el))) + .map(|&s| s.to_string()) + .collect::>(); - // Get hashtags - let hashtags = words - .iter() - .filter(|&&word| word.starts_with('#')) - .map(|&s| s.to_string()) - .collect::>(); + // Get hashtags + let hashtags = words + .iter() + .filter(|&&word| word.starts_with('#')) + .map(|&s| s.to_string()) + .collect::>(); - for mention in mentions { - let entity = mention.replace("nostr:", "").replace('@', ""); + for mention in mentions { + let entity = mention.replace("nostr:", "").replace('@', ""); - if !tag_set.contains(&entity) { - if entity.starts_with("npub") { - if let Ok(public_key) = PublicKey::from_bech32(&entity) { - let tag = Tag::public_key(public_key); - tags.push(tag); - } else { - continue; + if !tag_set.contains(&entity) { + if entity.starts_with("npub") { + if let Ok(public_key) = PublicKey::from_bech32(&entity) { + let tag = Tag::public_key(public_key); + tags.push(tag); + } else { + continue; + } + } + if entity.starts_with("nprofile") { + if let Ok(public_key) = PublicKey::from_bech32(&entity) { + let tag = Tag::public_key(public_key); + tags.push(tag); + } else { + continue; + } + } + if entity.starts_with("note") { + if let Ok(event_id) = EventId::from_bech32(&entity) { + let hex = event_id.to_hex(); + let tag = Tag::parse(&["e", &hex, "", "mention"]).unwrap(); + tags.push(tag); + } else { + continue; + } + } + if entity.starts_with("nevent") { + if let Ok(event) = Nip19Event::from_bech32(&entity) { + let hex = event.event_id.to_hex(); + let relay = event.clone().relays.into_iter().next().unwrap_or("".into()); + let tag = Tag::parse(&["e", &hex, &relay, "mention"]).unwrap(); + + if let Some(author) = event.author { + let tag = Tag::public_key(author); + tags.push(tag); + } + + tags.push(tag); + } else { + continue; + } + } + tag_set.insert(entity); } - } - if entity.starts_with("nprofile") { - if let Ok(public_key) = PublicKey::from_bech32(&entity) { - let tag = Tag::public_key(public_key); - tags.push(tag); - } else { - continue; - } - } - if entity.starts_with("note") { - if let Ok(event_id) = EventId::from_bech32(&entity) { - let hex = event_id.to_hex(); - let tag = Tag::parse(&["e", &hex, "", "mention"]).unwrap(); - tags.push(tag); - } else { - continue; - } - } - if entity.starts_with("nevent") { - if let Ok(event) = Nip19Event::from_bech32(&entity) { - let hex = event.event_id.to_hex(); - let relay = event.clone().relays.into_iter().next().unwrap_or("".into()); - let tag = Tag::parse(&["e", &hex, &relay, "mention"]).unwrap(); + } - if let Some(author) = event.author { - let tag = Tag::public_key(author); + for hashtag in hashtags { + if !tag_set.contains(&hashtag) { + let tag = Tag::hashtag(hashtag.clone()); tags.push(tag); - } - - tags.push(tag); - } else { - continue; + tag_set.insert(hashtag); } - } - tag_set.insert(entity); } - } - for hashtag in hashtags { - if !tag_set.contains(&hashtag) { - let tag = Tag::hashtag(hashtag.clone()); - tags.push(tag); - tag_set.insert(hashtag); - } - } - - tags + tags } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[tokio::test] - async fn test_parse_event() { - let content = "Check this image: https://example.com/image.jpg #cool @npub1"; - let meta = parse_event(content).await; + #[tokio::test] + async fn test_parse_event() { + let content = "Check this image: https://example.com/image.jpg #cool @npub1"; + let meta = parse_event(content).await; - assert_eq!(meta.content, "Check this image: #cool @npub1"); - assert_eq!(meta.images, vec!["https://example.com/image.jpg"]); - assert_eq!(meta.videos, Vec::::new()); - assert_eq!(meta.hashtags, vec!["#cool"]); - assert_eq!(meta.mentions, vec!["@npub1"]); - } + assert_eq!(meta.content, "Check this image: #cool @npub1"); + assert_eq!(meta.images, vec!["https://example.com/image.jpg"]); + assert_eq!(meta.videos, Vec::::new()); + assert_eq!(meta.hashtags, vec!["#cool"]); + assert_eq!(meta.mentions, vec!["@npub1"]); + } - #[tokio::test] - async fn test_parse_video() { - let content = "Check this video: https://example.com/video.mp4 #cool @npub1"; - let meta = parse_event(content).await; + #[tokio::test] + async fn test_parse_video() { + let content = "Check this video: https://example.com/video.mp4 #cool @npub1"; + let meta = parse_event(content).await; - assert_eq!(meta.content, "Check this video: #cool @npub1"); - assert_eq!(meta.images, Vec::::new()); - assert_eq!(meta.videos, vec!["https://example.com/video.mp4"]); - assert_eq!(meta.hashtags, vec!["#cool"]); - assert_eq!(meta.mentions, vec!["@npub1"]); - } + assert_eq!(meta.content, "Check this video: #cool @npub1"); + assert_eq!(meta.images, Vec::::new()); + assert_eq!(meta.videos, vec!["https://example.com/video.mp4"]); + assert_eq!(meta.hashtags, vec!["#cool"]); + assert_eq!(meta.mentions, vec!["@npub1"]); + } } diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 4adc5ef5..8cf8fa90 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -5,7 +5,6 @@ { "title": "Lume", "label": "main", - "titleBarStyle": "Overlay", "width": 1045, "height": 800, "minWidth": 480, diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index 4e8b1588..965e9f19 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -1,12 +1,6 @@ { "$schema": "../node_modules/@tauri-apps/cli/schema.json", "app": { - "trayIcon": { - "id": "main", - "iconPath": "./icons/tray.png", - "iconAsTemplate": true, - "menuOnLeftClick": false - }, "windows": [ { "title": "Lume",