From 055d73c8299291eb0db83561f4a307c6dade8e53 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 24 Oct 2024 15:50:45 +0700 Subject: [PATCH] feat: rework multi account --- src-tauri/src/commands/account.rs | 67 ++- src-tauri/src/commands/event.rs | 42 +- src-tauri/src/commands/metadata.rs | 26 +- src-tauri/src/commands/sync.rs | 150 +++++- src-tauri/src/main.rs | 53 +- src/commands.gen.ts | 26 +- src/components/note/buttons/repost.tsx | 37 +- src/routes.gen.ts | 251 ++++----- src/routes/__root.tsx | 19 +- .../{_layout.lazy.tsx => _app.lazy.tsx} | 32 +- src/routes/_app.tsx | 14 + src/routes/{_layout => _app}/index.lazy.tsx | 78 +-- src/routes/_app/index.tsx | 26 + src/routes/_layout.tsx | 14 - src/routes/_layout/index.tsx | 3 - src/routes/columns/_layout.tsx | 15 +- .../{auth => new-account}/connect.lazy.tsx | 2 +- .../{auth => new-account}/import.lazy.tsx | 2 +- .../{auth => new-account}/watch.lazy.tsx | 2 +- src/routes/new-post/index.lazy.tsx | 459 ++++++++++++++++ src/routes/new-post/index.tsx | 492 +----------------- src/routes/new.lazy.tsx | 6 +- src/routes/reset.lazy.tsx | 130 ----- src/routes/set-group.lazy.tsx | 16 + src/routes/set-interest.lazy.tsx | 17 + src/routes/set-signer.$id.lazy.tsx | 92 ---- src/routes/zap.$id.lazy.tsx | 70 +-- src/system/useEvent.ts | 33 +- src/system/useProfile.ts | 2 - src/system/useRect.ts | 2 +- src/types.ts | 8 +- 31 files changed, 979 insertions(+), 1207 deletions(-) rename src/routes/{_layout.lazy.tsx => _app.lazy.tsx} (87%) create mode 100644 src/routes/_app.tsx rename src/routes/{_layout => _app}/index.lazy.tsx (72%) create mode 100644 src/routes/_app/index.tsx delete mode 100644 src/routes/_layout.tsx delete mode 100644 src/routes/_layout/index.tsx rename src/routes/{auth => new-account}/connect.lazy.tsx (97%) rename src/routes/{auth => new-account}/import.lazy.tsx (98%) rename src/routes/{auth => new-account}/watch.lazy.tsx (97%) create mode 100644 src/routes/new-post/index.lazy.tsx delete mode 100644 src/routes/reset.lazy.tsx delete mode 100644 src/routes/set-signer.$id.lazy.tsx diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 41371760..ef9e8172 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -1,12 +1,15 @@ +use async_utility::thread::sleep; use keyring::Entry; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; use specta::Type; -use std::{str::FromStr, time::Duration}; -use tauri::{Emitter, State}; +use std::{fs, str::FromStr, time::Duration}; +use tauri::{Emitter, Manager, State}; use crate::{common::get_all_accounts, Nostr}; +use super::sync::sync_account; + #[derive(Debug, Clone, Serialize, Deserialize, Type)] struct Account { secret_key: String, @@ -21,16 +24,27 @@ pub fn get_accounts() -> Vec { #[tauri::command] #[specta::specta] -pub async fn watch_account(id: String, state: State<'_, Nostr>) -> Result { +pub async fn watch_account( + id: String, + state: State<'_, Nostr>, + app_handle: tauri::AppHandle, +) -> Result { let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; let npub = public_key.to_bech32().map_err(|e| e.to_string())?; let keyring = Entry::new("Lume Safe Storage", &npub).map_err(|e| e.to_string())?; // Set empty password keyring.set_password("").map_err(|e| e.to_string())?; + + // Run sync for this account + sync_account(public_key, app_handle); + // Update state state.accounts.lock().unwrap().push(npub.clone()); + // Fake loading + sleep(Duration::from_secs(4)).await; + Ok(npub) } @@ -40,6 +54,7 @@ pub async fn import_account( key: String, password: Option, state: State<'_, Nostr>, + app_handle: tauri::AppHandle, ) -> Result { let client = &state.client; @@ -54,10 +69,8 @@ pub async fn import_account( let hex = secret_key.to_secret_hex(); let keys = Keys::new(secret_key); - let npub = keys - .public_key() - .to_bech32() - .map_err(|err| err.to_string())?; + let public_key = keys.public_key(); + let npub = public_key.to_bech32().map_err(|err| err.to_string())?; let signer = NostrSigner::Keys(keys); let keyring = Entry::new("Lume Safe Storage", &npub).map_err(|e| e.to_string())?; @@ -73,6 +86,13 @@ pub async fn import_account( // Update signer client.set_signer(Some(signer)).await; + + // Run sync for this account + sync_account(public_key, app_handle); + + // Fake loading + sleep(Duration::from_secs(4)).await; + // Update state state.accounts.lock().unwrap().push(npub.clone()); @@ -81,7 +101,11 @@ pub async fn import_account( #[tauri::command] #[specta::specta] -pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result { +pub async fn connect_account( + uri: String, + state: State<'_, Nostr>, + app_handle: tauri::AppHandle, +) -> Result { let client = &state.client; match NostrConnectURI::parse(uri.clone()) { @@ -94,6 +118,9 @@ pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result { let mut url = Url::parse(&uri).unwrap(); @@ -117,6 +144,7 @@ pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result Result<(), String> { Ok(()) } +#[tauri::command] +#[specta::specta] +pub async fn is_new_account(id: String, app_handle: tauri::AppHandle) -> Result { + let config_dir = app_handle.path().config_dir().map_err(|e| e.to_string())?; + let exist = fs::metadata(config_dir.join(id)).is_ok(); + + Ok(!exist) +} + +#[tauri::command] +#[specta::specta] +pub async fn toggle_new_account(id: String, app_handle: tauri::AppHandle) -> Result<(), String> { + let config_dir = app_handle.path().config_dir().map_err(|e| e.to_string())?; + fs::File::create(config_dir.join(id)).unwrap(); + + Ok(()) +} + #[tauri::command] #[specta::specta] pub async fn has_signer(id: String, state: State<'_, Nostr>) -> Result { @@ -200,6 +246,11 @@ pub async fn set_signer( let account = match keyring.get_password() { Ok(pw) => { let account: Account = serde_json::from_str(&pw).map_err(|e| e.to_string())?; + + if account.secret_key.is_empty() { + return Err("Watch Only account".into()); + }; + account } Err(e) => return Err(e.to_string()), diff --git a/src-tauri/src/commands/event.rs b/src-tauri/src/commands/event.rs index 253e5207..7a7db3ee 100644 --- a/src-tauri/src/commands/event.rs +++ b/src-tauri/src/commands/event.rs @@ -290,7 +290,7 @@ pub async fn publish( warning: Option, difficulty: Option, state: State<'_, Nostr>, -) -> Result { +) -> Result { let client = &state.client; // Create event tags from content @@ -320,13 +320,8 @@ pub async fn publish( .map_err(|err| err.to_string())?; // Save to local database - match client.database().save_event(&event).await { - Ok(status) => { - // Add event to queue to broadcast it later. - state.send_queue.lock().unwrap().insert(event); - // Return - Ok(status) - } + match client.send_event(event).await { + Ok(output) => Ok(output.to_hex()), Err(err) => Err(err.to_string()), } } @@ -338,7 +333,7 @@ pub async fn reply( to: String, root: Option, state: State<'_, Nostr>, -) -> Result { +) -> Result { let client = &state.client; // Create event tags from content @@ -380,39 +375,20 @@ pub async fn reply( .await .map_err(|err| err.to_string())?; - // Save to local database - match client.database().save_event(&event).await { - Ok(status) => { - // Add event to queue to broadcast it later. - state.send_queue.lock().unwrap().insert(event); - // Return - Ok(status) - } + match client.send_event(event).await { + Ok(output) => Ok(output.to_hex()), Err(err) => Err(err.to_string()), } } #[tauri::command] #[specta::specta] -pub async fn repost(raw: String, state: State<'_, Nostr>) -> Result { +pub async fn repost(raw: String, state: State<'_, Nostr>) -> Result { let client = &state.client; let event = Event::from_json(raw).map_err(|err| err.to_string())?; - let builder = EventBuilder::repost(&event, None); - // Sign event - let event = client - .sign_event_builder(builder) - .await - .map_err(|err| err.to_string())?; - - // Save to local database - match client.database().save_event(&event).await { - Ok(status) => { - // Add event to queue to broadcast it later. - state.send_queue.lock().unwrap().insert(event); - // Return - Ok(status) - } + match client.repost(&event, None).await { + Ok(output) => Ok(output.to_hex()), Err(err) => Err(err.to_string()), } } diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index 8827d25e..2585727c 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -199,7 +199,7 @@ pub async fn set_group( users: Vec, state: State<'_, Nostr>, handle: tauri::AppHandle, -) -> Result { +) -> Result { let client = &state.client; let public_keys: Vec = users .iter() @@ -225,12 +225,8 @@ pub async fn set_group( .await .map_err(|err| err.to_string())?; - // Save to local database - match client.database().save_event(&event).await { - Ok(status) => { - // Add event to queue to broadcast it later. - state.send_queue.lock().unwrap().insert(event); - + match client.send_event(event).await { + Ok(output) => { // Sync event tauri::async_runtime::spawn(async move { let state = handle.state::(); @@ -247,8 +243,7 @@ pub async fn set_group( }; }); - // Return - Ok(status) + Ok(output.to_hex()) } Err(err) => Err(err.to_string()), } @@ -302,7 +297,7 @@ pub async fn set_interest( hashtags: Vec, state: State<'_, Nostr>, handle: tauri::AppHandle, -) -> Result { +) -> Result { let client = &state.client; let label = title.to_lowercase().replace(" ", "-"); let mut tags: Vec = vec![Tag::title(title)]; @@ -324,12 +319,8 @@ pub async fn set_interest( .await .map_err(|err| err.to_string())?; - // Save to local database - match client.database().save_event(&event).await { - Ok(status) => { - // Add event to queue to broadcast it later. - state.send_queue.lock().unwrap().insert(event); - + match client.send_event(event).await { + Ok(output) => { // Sync event tauri::async_runtime::spawn(async move { let state = handle.state::(); @@ -346,8 +337,7 @@ pub async fn set_interest( }; }); - // Return - Ok(status) + Ok(output.to_hex()) } Err(err) => Err(err.to_string()), } diff --git a/src-tauri/src/commands/sync.rs b/src-tauri/src/commands/sync.rs index 3700abea..9816ce90 100644 --- a/src-tauri/src/commands/sync.rs +++ b/src-tauri/src/commands/sync.rs @@ -24,7 +24,7 @@ pub enum NegentropyKind { Others, } -pub fn run_fast_sync(accounts: Vec, app_handle: AppHandle) { +pub fn sync_all(accounts: Vec, app_handle: AppHandle) { if accounts.is_empty() { return; }; @@ -45,51 +45,70 @@ pub fn run_fast_sync(accounts: Vec, app_handle: AppHandle) { let client = &state.client; let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone(); - // NEG: Sync profile + // NEG: Sync metadata // - let profile = Filter::new() - .authors(public_keys.clone()) - .kind(Kind::Metadata) - .limit(4); + let metadata = Filter::new().authors(public_keys.clone()).kinds(vec![ + Kind::Metadata, + Kind::ContactList, + Kind::Interests, + Kind::InterestSet, + Kind::FollowSet, + Kind::EventDeletion, + Kind::TextNote, + Kind::Repost, + Kind::Custom(30315), + ]); if let Ok(report) = client - .sync_with(&bootstrap_relays, profile, NegentropyOptions::default()) + .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) .await { NegentropyEvent { - kind: NegentropyKind::Profile, + kind: NegentropyKind::Others, total_event: report.received.len() as i32, } .emit(&app_handle) .unwrap(); } - // NEG: Sync contact list + // NEG: Sync notification // - let contact_list = Filter::new() - .authors(public_keys.clone()) - .kind(Kind::ContactList) - .limit(4); + let notification = Filter::new() + .pubkeys(public_keys) + .kinds(vec![ + Kind::TextNote, + Kind::Repost, + Kind::Reaction, + Kind::ZapReceipt, + ]) + .limit(5000); if let Ok(report) = client .sync_with( &bootstrap_relays, - contact_list.clone(), + notification, NegentropyOptions::default(), ) .await { NegentropyEvent { - kind: NegentropyKind::Metadata, + kind: NegentropyKind::Notification, total_event: report.received.len() as i32, } .emit(&app_handle) .unwrap(); } - // NEG: Sync events from contact list + // NEG: Sync events for all pubkeys in local database // - if let Ok(events) = client.database().query(vec![contact_list]).await { + let pubkey_filter = Filter::new().kinds(vec![ + Kind::ContactList, + Kind::Repost, + Kind::TextNote, + Kind::FollowSet, + ]); + + if let Ok(events) = client.database().query(vec![pubkey_filter]).await { let pubkeys: Vec = events .iter() .flat_map(|ev| ev.tags.public_keys().copied()) @@ -107,7 +126,7 @@ pub fn run_fast_sync(accounts: Vec, app_handle: AppHandle) { let events = Filter::new() .authors(authors.clone()) .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(1000); + .limit(5000); if let Ok(report) = client .sync_with(&bootstrap_relays, events, NegentropyOptions::default()) @@ -125,8 +144,7 @@ pub fn run_fast_sync(accounts: Vec, app_handle: AppHandle) { // let metadata = Filter::new() .authors(authors) - .kind(Kind::Metadata) - .limit(1000); + .kinds(vec![Kind::Metadata, Kind::ContactList]); if let Ok(report) = client .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) @@ -141,40 +159,116 @@ pub fn run_fast_sync(accounts: Vec, app_handle: AppHandle) { } } } + }); +} - // NEG: Sync other metadata +pub fn sync_account(public_key: PublicKey, app_handle: AppHandle) { + tauri::async_runtime::spawn(async move { + let state = app_handle.state::(); + let client = &state.client; + let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone(); + + // NEG: Sync all user's metadata // - let others = Filter::new().authors(public_keys.clone()).kinds(vec![ + let metadata = Filter::new().author(public_key).kinds(vec![ + Kind::Metadata, + Kind::ContactList, Kind::Interests, Kind::InterestSet, Kind::FollowSet, + Kind::RelayList, + Kind::RelaySet, Kind::EventDeletion, Kind::Custom(30315), ]); if let Ok(report) = client - .sync_with(&bootstrap_relays, others, NegentropyOptions::default()) + .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) .await { NegentropyEvent { - kind: NegentropyKind::Others, + kind: NegentropyKind::Metadata, total_event: report.received.len() as i32, } .emit(&app_handle) .unwrap(); } - // NEG: Sync notification + if let Ok(contact_list) = client.database().contacts_public_keys(public_key).await { + // NEG: Sync all contact's metadata + // + let metadata = Filter::new() + .authors(contact_list.clone()) + .kinds(vec![Kind::Metadata, Kind::RelaySet, Kind::Custom(30315)]) + .limit(1000); + + if let Ok(report) = client + .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) + .await + { + NegentropyEvent { + kind: NegentropyKind::Metadata, + total_event: report.received.len() as i32, + } + .emit(&app_handle) + .unwrap(); + } + + // NEG: Sync all contact's events + // + let metadata = Filter::new() + .authors(contact_list.clone()) + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(1000); + + if let Ok(report) = client + .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) + .await + { + NegentropyEvent { + kind: NegentropyKind::Events, + total_event: report.received.len() as i32, + } + .emit(&app_handle) + .unwrap(); + } + + // NEG: Sync all contact's other metadata + // + let metadata = Filter::new() + .authors(contact_list) + .kinds(vec![ + Kind::Interests, + Kind::InterestSet, + Kind::FollowSet, + Kind::EventDeletion, + ]) + .limit(1000); + + if let Ok(report) = client + .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) + .await + { + NegentropyEvent { + kind: NegentropyKind::Metadata, + total_event: report.received.len() as i32, + } + .emit(&app_handle) + .unwrap(); + } + } + + // NEG: Sync all user's metadata // let notification = Filter::new() - .pubkeys(public_keys) + .pubkey(public_key) .kinds(vec![ - Kind::Reaction, Kind::TextNote, Kind::Repost, + Kind::Reaction, Kind::ZapReceipt, ]) - .limit(10000); + .limit(500); if let Ok(report) = client .sync_with( diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ee869542..da505d96 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,7 +5,14 @@ #[cfg(target_os = "macos")] use border::WebviewWindowExt as BorderWebviewWindowExt; -use commands::{account::*, event::*, metadata::*, relay::*, sync::NegentropyEvent, window::*}; +use commands::{ + account::*, + event::*, + metadata::*, + relay::*, + sync::{sync_all, NegentropyEvent}, + window::*, +}; use common::{get_all_accounts, parse_event}; use nostr_sdk::prelude::{Profile as DatabaseProfile, *}; use serde::{Deserialize, Serialize}; @@ -19,7 +26,7 @@ use std::{ sync::Mutex, time::Duration, }; -use tauri::{path::BaseDirectory, Emitter, EventTarget, Manager, WindowEvent}; +use tauri::{path::BaseDirectory, Emitter, EventTarget, Manager}; use tauri_plugin_decorum::WebviewWindowExt; use tauri_plugin_notification::{NotificationExt, PermissionState}; use tauri_specta::{collect_commands, collect_events, Builder, Event as TauriEvent}; @@ -33,7 +40,6 @@ pub struct Nostr { accounts: Mutex>, bootstrap_relays: Mutex>, subscriptions: Mutex>, - send_queue: Mutex>, } #[derive(Clone, Serialize, Deserialize, Type)] @@ -86,7 +92,7 @@ struct Sync { id: String, } -pub const DEFAULT_DIFFICULTY: u8 = 21; +pub const DEFAULT_DIFFICULTY: u8 = 0; pub const FETCH_LIMIT: usize = 50; pub const NOTIFICATION_SUB_ID: &str = "lume_notification"; @@ -105,6 +111,8 @@ fn main() { get_private_key, delete_account, reset_password, + is_new_account, + toggle_new_account, has_signer, set_signer, get_profile, @@ -175,6 +183,7 @@ fn main() { let handle = app.handle(); let handle_clone = handle.clone(); let handle_clone_child = handle_clone.clone(); + let handle_clone_child_child = handle_clone_child.clone(); let main_window = app.get_webview_window("main").unwrap(); let config_dir = handle @@ -260,6 +269,8 @@ fn main() { }); let accounts = get_all_accounts(); + // Run sync for all accounts + sync_all(accounts.clone(), handle_clone_child_child); // Create global state app.manage(Nostr { @@ -268,7 +279,6 @@ fn main() { settings: Mutex::new(Settings::default()), bootstrap_relays: Mutex::new(bootstrap_relays), subscriptions: Mutex::new(HashSet::new()), - send_queue: Mutex::new(HashSet::new()), }); // Handle subscription request @@ -540,39 +550,6 @@ fn main() { Ok(()) }) - .on_window_event(|window, event| match event { - WindowEvent::CloseRequested { api, .. } => { - api.prevent_close(); - // Just hide window not close - window.hide().unwrap(); - - let state = window.state::(); - let client = &state.client; - let queue: Vec = state - .send_queue - .lock() - .unwrap() - .clone() - .into_iter() - .collect(); - - if !queue.is_empty() { - tauri::async_runtime::block_on(async { - println!("Sending total {} events to relays", queue.len()); - match client.batch_event(queue, RelaySendOptions::default()).await { - Ok(_) => window.destroy().unwrap(), - Err(_) => window.emit("batch-event", ()).unwrap(), - } - }); - } else { - window.destroy().unwrap() - } - } - WindowEvent::Focused(_focused) => { - // TODO - } - _ => {} - }) .plugin(prevent_default()) .plugin(tauri_plugin_theme::init(ctx.config_mut())) .plugin(tauri_plugin_decorum::init()) diff --git a/src/commands.gen.ts b/src/commands.gen.ts index 35f68f24..a6c54ce5 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -96,6 +96,22 @@ async resetPassword(key: string, password: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_new_account", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async toggleNewAccount(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("toggle_new_account", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async hasSigner(id: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("has_signer", { id }) }; @@ -168,7 +184,7 @@ async getAllProfiles() : Promise> { else return { status: "error", error: e as any }; } }, -async setGroup(title: string, description: string | null, image: string | null, users: string[]) : Promise> { +async setGroup(title: string, description: string | null, image: string | null, users: string[]) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("set_group", { title, description, image, users }) }; } catch (e) { @@ -192,7 +208,7 @@ async getAllGroups() : Promise> { else return { status: "error", error: e as any }; } }, -async setInterest(title: string, description: string | null, image: string | null, hashtags: string[]) : Promise> { +async setInterest(title: string, description: string | null, image: string | null, hashtags: string[]) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("set_interest", { title, description, image, hashtags }) }; } catch (e) { @@ -384,7 +400,7 @@ async search(query: string) : Promise> { else return { status: "error", error: e as any }; } }, -async publish(content: string, warning: string | null, difficulty: number | null) : Promise> { +async publish(content: string, warning: string | null, difficulty: number | null) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("publish", { content, warning, difficulty }) }; } catch (e) { @@ -392,7 +408,7 @@ async publish(content: string, warning: string | null, difficulty: number | null else return { status: "error", error: e as any }; } }, -async reply(content: string, to: string, root: string | null) : Promise> { +async reply(content: string, to: string, root: string | null) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("reply", { content, to, root }) }; } catch (e) { @@ -400,7 +416,7 @@ async reply(content: string, to: string, root: string | null) : Promise> { +async repost(raw: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("repost", { raw }) }; } catch (e) { diff --git a/src/components/note/buttons/repost.tsx b/src/components/note/buttons/repost.tsx index e73caccb..bf6215fb 100644 --- a/src/components/note/buttons/repost.tsx +++ b/src/components/note/buttons/repost.tsx @@ -1,14 +1,13 @@ import { commands } from "@/commands.gen"; import { appSettings, cn, displayNpub } from "@/commons"; import { RepostIcon, Spinner } from "@/components"; -import { LumeWindow } from "@/system"; import type { Metadata } from "@/types"; import * as Tooltip from "@radix-ui/react-tooltip"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useStore } from "@tanstack/react-store"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; -import type { Window } from "@tauri-apps/api/window"; -import { useCallback, useEffect, useState, useTransition } from "react"; +import { message } from "@tauri-apps/plugin-dialog"; +import { useCallback, useTransition } from "react"; import { useNoteContext } from "../provider"; export function NoteRepost({ @@ -38,13 +37,12 @@ export function NoteRepost({ }); const [isPending, startTransition] = useTransition(); - const [popup, setPopup] = useState(null); const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); const accounts = await commands.getAccounts(); - const list = []; + const list: Promise[] = []; for (const account of accounts) { const res = await commands.getProfile(account); @@ -52,7 +50,7 @@ export function NoteRepost({ if (res.status === "ok") { const profile: Metadata = JSON.parse(res.data); - name = profile.display_name ?? profile.name; + name = profile.display_name ?? profile.name ?? "unknown"; } list.push( @@ -102,14 +100,14 @@ export function NoteRepost({ if (signer.status === "ok") { if (!signer.data) { - const newPopup = await LumeWindow.openPopup( - `/set-signer/${account}`, - undefined, - false, - ); + if (!signer.data) { + const res = await commands.setSigner(account); - setPopup(newPopup); - return; + if (res.status === "error") { + await message(res.error, { kind: "error" }); + return; + } + } } repost.mutate(); @@ -122,19 +120,6 @@ export function NoteRepost({ }); }; - useEffect(() => { - if (!visible) return; - if (!popup) return; - - const unlisten = popup.listen("signer-updated", async () => { - repost.mutate(); - }); - - return () => { - unlisten.then((f) => f()); - }; - }, [popup]); - if (!visible) return null; return ( diff --git a/src/routes.gen.ts b/src/routes.gen.ts index 9251512a..f88a48f5 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -16,9 +16,9 @@ import { Route as rootRoute } from './routes/__root' import { Route as SetInterestImport } from './routes/set-interest' import { Route as SetGroupImport } from './routes/set-group' import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays' -import { Route as LayoutImport } from './routes/_layout' +import { Route as AppImport } from './routes/_app' import { Route as NewPostIndexImport } from './routes/new-post/index' -import { Route as LayoutIndexImport } from './routes/_layout/index' +import { Route as AppIndexImport } from './routes/_app/index' import { Route as ZapIdImport } from './routes/zap.$id' import { Route as ColumnsLayoutImport } from './routes/columns/_layout' import { Route as SettingsIdWalletImport } from './routes/settings.$id/wallet' @@ -37,13 +37,11 @@ import { Route as ColumnsLayoutCreateNewsfeedF2fImport } from './routes/columns/ // Create Virtual Routes const ColumnsImport = createFileRoute('/columns')() -const ResetLazyImport = createFileRoute('/reset')() const NewLazyImport = createFileRoute('/new')() const SettingsIdLazyImport = createFileRoute('/settings/$id')() -const SetSignerIdLazyImport = createFileRoute('/set-signer/$id')() -const AuthWatchLazyImport = createFileRoute('/auth/watch')() -const AuthImportLazyImport = createFileRoute('/auth/import')() -const AuthConnectLazyImport = createFileRoute('/auth/connect')() +const NewAccountWatchLazyImport = createFileRoute('/new-account/watch')() +const NewAccountImportLazyImport = createFileRoute('/new-account/import')() +const NewAccountConnectLazyImport = createFileRoute('/new-account/connect')() const ColumnsLayoutTrendingLazyImport = createFileRoute( '/columns/_layout/trending', )() @@ -76,11 +74,6 @@ const ColumnsRoute = ColumnsImport.update({ getParentRoute: () => rootRoute, } as any) -const ResetLazyRoute = ResetLazyImport.update({ - path: '/reset', - getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/reset.lazy').then((d) => d.Route)) - const NewLazyRoute = NewLazyImport.update({ path: '/new', getParentRoute: () => rootRoute, @@ -103,47 +96,48 @@ const BootstrapRelaysRoute = BootstrapRelaysImport.update({ import('./routes/bootstrap-relays.lazy').then((d) => d.Route), ) -const LayoutRoute = LayoutImport.update({ - id: '/_layout', +const AppRoute = AppImport.update({ + id: '/_app', getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/_layout.lazy').then((d) => d.Route)) +} as any).lazy(() => import('./routes/_app.lazy').then((d) => d.Route)) const NewPostIndexRoute = NewPostIndexImport.update({ path: '/new-post/', getParentRoute: () => rootRoute, -} as any) +} as any).lazy(() => + import('./routes/new-post/index.lazy').then((d) => d.Route), +) -const LayoutIndexRoute = LayoutIndexImport.update({ +const AppIndexRoute = AppIndexImport.update({ path: '/', - getParentRoute: () => LayoutRoute, -} as any).lazy(() => import('./routes/_layout/index.lazy').then((d) => d.Route)) + getParentRoute: () => AppRoute, +} as any).lazy(() => import('./routes/_app/index.lazy').then((d) => d.Route)) const SettingsIdLazyRoute = SettingsIdLazyImport.update({ path: '/settings/$id', getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/settings.$id.lazy').then((d) => d.Route)) -const SetSignerIdLazyRoute = SetSignerIdLazyImport.update({ - path: '/set-signer/$id', +const NewAccountWatchLazyRoute = NewAccountWatchLazyImport.update({ + path: '/new-account/watch', getParentRoute: () => rootRoute, } as any).lazy(() => - import('./routes/set-signer.$id.lazy').then((d) => d.Route), + import('./routes/new-account/watch.lazy').then((d) => d.Route), ) -const AuthWatchLazyRoute = AuthWatchLazyImport.update({ - path: '/auth/watch', +const NewAccountImportLazyRoute = NewAccountImportLazyImport.update({ + path: '/new-account/import', getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/auth/watch.lazy').then((d) => d.Route)) +} as any).lazy(() => + import('./routes/new-account/import.lazy').then((d) => d.Route), +) -const AuthImportLazyRoute = AuthImportLazyImport.update({ - path: '/auth/import', +const NewAccountConnectLazyRoute = NewAccountConnectLazyImport.update({ + path: '/new-account/connect', getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/auth/import.lazy').then((d) => d.Route)) - -const AuthConnectLazyRoute = AuthConnectLazyImport.update({ - path: '/auth/connect', - getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/auth/connect.lazy').then((d) => d.Route)) +} as any).lazy(() => + import('./routes/new-account/connect.lazy').then((d) => d.Route), +) const ZapIdRoute = ZapIdImport.update({ path: '/zap/$id', @@ -302,11 +296,11 @@ const ColumnsLayoutCreateNewsfeedF2fRoute = declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/_layout': { - id: '/_layout' + '/_app': { + id: '/_app' path: '' fullPath: '' - preLoaderRoute: typeof LayoutImport + preLoaderRoute: typeof AppImport parentRoute: typeof rootRoute } '/bootstrap-relays': { @@ -337,13 +331,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof NewLazyImport parentRoute: typeof rootRoute } - '/reset': { - id: '/reset' - path: '/reset' - fullPath: '/reset' - preLoaderRoute: typeof ResetLazyImport - parentRoute: typeof rootRoute - } '/columns': { id: '/columns' path: '/columns' @@ -365,32 +352,25 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ZapIdImport parentRoute: typeof rootRoute } - '/auth/connect': { - id: '/auth/connect' - path: '/auth/connect' - fullPath: '/auth/connect' - preLoaderRoute: typeof AuthConnectLazyImport + '/new-account/connect': { + id: '/new-account/connect' + path: '/new-account/connect' + fullPath: '/new-account/connect' + preLoaderRoute: typeof NewAccountConnectLazyImport parentRoute: typeof rootRoute } - '/auth/import': { - id: '/auth/import' - path: '/auth/import' - fullPath: '/auth/import' - preLoaderRoute: typeof AuthImportLazyImport + '/new-account/import': { + id: '/new-account/import' + path: '/new-account/import' + fullPath: '/new-account/import' + preLoaderRoute: typeof NewAccountImportLazyImport parentRoute: typeof rootRoute } - '/auth/watch': { - id: '/auth/watch' - path: '/auth/watch' - fullPath: '/auth/watch' - preLoaderRoute: typeof AuthWatchLazyImport - parentRoute: typeof rootRoute - } - '/set-signer/$id': { - id: '/set-signer/$id' - path: '/set-signer/$id' - fullPath: '/set-signer/$id' - preLoaderRoute: typeof SetSignerIdLazyImport + '/new-account/watch': { + id: '/new-account/watch' + path: '/new-account/watch' + fullPath: '/new-account/watch' + preLoaderRoute: typeof NewAccountWatchLazyImport parentRoute: typeof rootRoute } '/settings/$id': { @@ -400,12 +380,12 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsIdLazyImport parentRoute: typeof rootRoute } - '/_layout/': { - id: '/_layout/' + '/_app/': { + id: '/_app/' path: '/' fullPath: '/' - preLoaderRoute: typeof LayoutIndexImport - parentRoute: typeof LayoutImport + preLoaderRoute: typeof AppIndexImport + parentRoute: typeof AppImport } '/new-post/': { id: '/new-post/' @@ -559,16 +539,15 @@ declare module '@tanstack/react-router' { // Create and export the route tree -interface LayoutRouteChildren { - LayoutIndexRoute: typeof LayoutIndexRoute +interface AppRouteChildren { + AppIndexRoute: typeof AppIndexRoute } -const LayoutRouteChildren: LayoutRouteChildren = { - LayoutIndexRoute: LayoutIndexRoute, +const AppRouteChildren: AppRouteChildren = { + AppIndexRoute: AppIndexRoute, } -const LayoutRouteWithChildren = - LayoutRoute._addFileChildren(LayoutRouteChildren) +const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren) interface ColumnsLayoutCreateNewsfeedRouteChildren { ColumnsLayoutCreateNewsfeedF2fRoute: typeof ColumnsLayoutCreateNewsfeedF2fRoute @@ -656,20 +635,18 @@ const SettingsIdLazyRouteWithChildren = SettingsIdLazyRoute._addFileChildren( ) export interface FileRoutesByFullPath { - '': typeof LayoutRouteWithChildren + '': typeof AppRouteWithChildren '/bootstrap-relays': typeof BootstrapRelaysRoute '/set-group': typeof SetGroupRoute '/set-interest': typeof SetInterestRoute '/new': typeof NewLazyRoute - '/reset': typeof ResetLazyRoute '/columns': typeof ColumnsLayoutRouteWithChildren '/zap/$id': typeof ZapIdRoute - '/auth/connect': typeof AuthConnectLazyRoute - '/auth/import': typeof AuthImportLazyRoute - '/auth/watch': typeof AuthWatchLazyRoute - '/set-signer/$id': typeof SetSignerIdLazyRoute + '/new-account/connect': typeof NewAccountConnectLazyRoute + '/new-account/import': typeof NewAccountImportLazyRoute + '/new-account/watch': typeof NewAccountWatchLazyRoute '/settings/$id': typeof SettingsIdLazyRouteWithChildren - '/': typeof LayoutIndexRoute + '/': typeof AppIndexRoute '/new-post': typeof NewPostIndexRoute '/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/global': typeof ColumnsLayoutGlobalRoute @@ -698,15 +675,13 @@ export interface FileRoutesByTo { '/set-group': typeof SetGroupRoute '/set-interest': typeof SetInterestRoute '/new': typeof NewLazyRoute - '/reset': typeof ResetLazyRoute '/columns': typeof ColumnsLayoutRouteWithChildren '/zap/$id': typeof ZapIdRoute - '/auth/connect': typeof AuthConnectLazyRoute - '/auth/import': typeof AuthImportLazyRoute - '/auth/watch': typeof AuthWatchLazyRoute - '/set-signer/$id': typeof SetSignerIdLazyRoute + '/new-account/connect': typeof NewAccountConnectLazyRoute + '/new-account/import': typeof NewAccountImportLazyRoute + '/new-account/watch': typeof NewAccountWatchLazyRoute '/settings/$id': typeof SettingsIdLazyRouteWithChildren - '/': typeof LayoutIndexRoute + '/': typeof AppIndexRoute '/new-post': typeof NewPostIndexRoute '/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/global': typeof ColumnsLayoutGlobalRoute @@ -732,21 +707,19 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRoute - '/_layout': typeof LayoutRouteWithChildren + '/_app': typeof AppRouteWithChildren '/bootstrap-relays': typeof BootstrapRelaysRoute '/set-group': typeof SetGroupRoute '/set-interest': typeof SetInterestRoute '/new': typeof NewLazyRoute - '/reset': typeof ResetLazyRoute '/columns': typeof ColumnsRouteWithChildren '/columns/_layout': typeof ColumnsLayoutRouteWithChildren '/zap/$id': typeof ZapIdRoute - '/auth/connect': typeof AuthConnectLazyRoute - '/auth/import': typeof AuthImportLazyRoute - '/auth/watch': typeof AuthWatchLazyRoute - '/set-signer/$id': typeof SetSignerIdLazyRoute + '/new-account/connect': typeof NewAccountConnectLazyRoute + '/new-account/import': typeof NewAccountImportLazyRoute + '/new-account/watch': typeof NewAccountWatchLazyRoute '/settings/$id': typeof SettingsIdLazyRouteWithChildren - '/_layout/': typeof LayoutIndexRoute + '/_app/': typeof AppIndexRoute '/new-post/': typeof NewPostIndexRoute '/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/_layout/global': typeof ColumnsLayoutGlobalRoute @@ -778,13 +751,11 @@ export interface FileRouteTypes { | '/set-group' | '/set-interest' | '/new' - | '/reset' | '/columns' | '/zap/$id' - | '/auth/connect' - | '/auth/import' - | '/auth/watch' - | '/set-signer/$id' + | '/new-account/connect' + | '/new-account/import' + | '/new-account/watch' | '/settings/$id' | '/' | '/new-post' @@ -814,13 +785,11 @@ export interface FileRouteTypes { | '/set-group' | '/set-interest' | '/new' - | '/reset' | '/columns' | '/zap/$id' - | '/auth/connect' - | '/auth/import' - | '/auth/watch' - | '/set-signer/$id' + | '/new-account/connect' + | '/new-account/import' + | '/new-account/watch' | '/settings/$id' | '/' | '/new-post' @@ -846,21 +815,19 @@ export interface FileRouteTypes { | '/columns/users/$id' id: | '__root__' - | '/_layout' + | '/_app' | '/bootstrap-relays' | '/set-group' | '/set-interest' | '/new' - | '/reset' | '/columns' | '/columns/_layout' | '/zap/$id' - | '/auth/connect' - | '/auth/import' - | '/auth/watch' - | '/set-signer/$id' + | '/new-account/connect' + | '/new-account/import' + | '/new-account/watch' | '/settings/$id' - | '/_layout/' + | '/_app/' | '/new-post/' | '/columns/_layout/create-newsfeed' | '/columns/_layout/global' @@ -886,35 +853,31 @@ export interface FileRouteTypes { } export interface RootRouteChildren { - LayoutRoute: typeof LayoutRouteWithChildren + AppRoute: typeof AppRouteWithChildren BootstrapRelaysRoute: typeof BootstrapRelaysRoute SetGroupRoute: typeof SetGroupRoute SetInterestRoute: typeof SetInterestRoute NewLazyRoute: typeof NewLazyRoute - ResetLazyRoute: typeof ResetLazyRoute ColumnsRoute: typeof ColumnsRouteWithChildren ZapIdRoute: typeof ZapIdRoute - AuthConnectLazyRoute: typeof AuthConnectLazyRoute - AuthImportLazyRoute: typeof AuthImportLazyRoute - AuthWatchLazyRoute: typeof AuthWatchLazyRoute - SetSignerIdLazyRoute: typeof SetSignerIdLazyRoute + NewAccountConnectLazyRoute: typeof NewAccountConnectLazyRoute + NewAccountImportLazyRoute: typeof NewAccountImportLazyRoute + NewAccountWatchLazyRoute: typeof NewAccountWatchLazyRoute SettingsIdLazyRoute: typeof SettingsIdLazyRouteWithChildren NewPostIndexRoute: typeof NewPostIndexRoute } const rootRouteChildren: RootRouteChildren = { - LayoutRoute: LayoutRouteWithChildren, + AppRoute: AppRouteWithChildren, BootstrapRelaysRoute: BootstrapRelaysRoute, SetGroupRoute: SetGroupRoute, SetInterestRoute: SetInterestRoute, NewLazyRoute: NewLazyRoute, - ResetLazyRoute: ResetLazyRoute, ColumnsRoute: ColumnsRouteWithChildren, ZapIdRoute: ZapIdRoute, - AuthConnectLazyRoute: AuthConnectLazyRoute, - AuthImportLazyRoute: AuthImportLazyRoute, - AuthWatchLazyRoute: AuthWatchLazyRoute, - SetSignerIdLazyRoute: SetSignerIdLazyRoute, + NewAccountConnectLazyRoute: NewAccountConnectLazyRoute, + NewAccountImportLazyRoute: NewAccountImportLazyRoute, + NewAccountWatchLazyRoute: NewAccountWatchLazyRoute, SettingsIdLazyRoute: SettingsIdLazyRouteWithChildren, NewPostIndexRoute: NewPostIndexRoute, } @@ -931,26 +894,24 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/_layout", + "/_app", "/bootstrap-relays", "/set-group", "/set-interest", "/new", - "/reset", "/columns", "/zap/$id", - "/auth/connect", - "/auth/import", - "/auth/watch", - "/set-signer/$id", + "/new-account/connect", + "/new-account/import", + "/new-account/watch", "/settings/$id", "/new-post/" ] }, - "/_layout": { - "filePath": "_layout.tsx", + "/_app": { + "filePath": "_app.tsx", "children": [ - "/_layout/" + "/_app/" ] }, "/bootstrap-relays": { @@ -965,9 +926,6 @@ export const routeTree = rootRoute "/new": { "filePath": "new.lazy.tsx" }, - "/reset": { - "filePath": "reset.lazy.tsx" - }, "/columns": { "filePath": "columns", "children": [ @@ -997,17 +955,14 @@ export const routeTree = rootRoute "/zap/$id": { "filePath": "zap.$id.tsx" }, - "/auth/connect": { - "filePath": "auth/connect.lazy.tsx" + "/new-account/connect": { + "filePath": "new-account/connect.lazy.tsx" }, - "/auth/import": { - "filePath": "auth/import.lazy.tsx" + "/new-account/import": { + "filePath": "new-account/import.lazy.tsx" }, - "/auth/watch": { - "filePath": "auth/watch.lazy.tsx" - }, - "/set-signer/$id": { - "filePath": "set-signer.$id.lazy.tsx" + "/new-account/watch": { + "filePath": "new-account/watch.lazy.tsx" }, "/settings/$id": { "filePath": "settings.$id.lazy.tsx", @@ -1018,9 +973,9 @@ export const routeTree = rootRoute "/settings/$id/wallet" ] }, - "/_layout/": { - "filePath": "_layout/index.tsx", - "parent": "/_layout" + "/_app/": { + "filePath": "_app/index.tsx", + "parent": "/_app" }, "/new-post/": { "filePath": "new-post/index.tsx" diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index d468a39f..19a95913 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,5 +1,4 @@ import { events } from "@/commands.gen"; -import { appSettings } from "@/commons"; import { Spinner } from "@/components"; import type { QueryClient } from "@tanstack/react-query"; import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; @@ -20,24 +19,12 @@ export const Route = createRootRouteWithContext()({ function Screen() { const { queryClient } = Route.useRouteContext(); - /* - useEffect(() => { - const unlisten = events.newSettings.listen((data) => { - appSettings.setState((state) => { - return { ...state, ...data.payload }; - }); - }); - - return () => { - unlisten.then((f) => f()); - }; - }, []); - */ - useEffect(() => { const unlisten = events.negentropyEvent.listen(async (data) => { const queryKey = [data.payload.kind.toLowerCase()]; - await queryClient.invalidateQueries({ queryKey }); + console.info("invalidate: ", queryKey); + + await queryClient.refetchQueries({ queryKey }); }); return () => { diff --git a/src/routes/_layout.lazy.tsx b/src/routes/_app.lazy.tsx similarity index 87% rename from src/routes/_layout.lazy.tsx rename to src/routes/_app.lazy.tsx index 8de76b7d..edbffebc 100644 --- a/src/routes/_layout.lazy.tsx +++ b/src/routes/_app.lazy.tsx @@ -1,17 +1,22 @@ import { commands } from "@/commands.gen"; -import { cn } from "@/commons"; +import { appColumns, cn } from "@/commons"; import { PublishIcon } from "@/components"; import { User } from "@/components/user"; import { LumeWindow } from "@/system"; import { MagnifyingGlass, Plus } from "@phosphor-icons/react"; import { useQuery } from "@tanstack/react-query"; -import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router"; +import { + Link, + Outlet, + createLazyFileRoute, + useRouter, +} from "@tanstack/react-router"; import { listen } from "@tauri-apps/api/event"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { useCallback, useEffect } from "react"; -export const Route = createLazyFileRoute("/_layout")({ +export const Route = createLazyFileRoute("/_app")({ component: Layout, }); @@ -83,6 +88,7 @@ function Topbar() { function Account({ pubkey }: { pubkey: string }) { const navigate = Route.useNavigate(); const context = Route.useRouteContext(); + const router = useRouter(); const { data: isActive } = useQuery({ queryKey: ["signer", pubkey], @@ -103,10 +109,9 @@ function Account({ pubkey }: { pubkey: string }) { const items = await Promise.all([ MenuItem.new({ - text: "Unlock", + text: "Activate", enabled: !isActive || true, - action: () => - LumeWindow.openPopup(`/set-signer/${pubkey}`, undefined, false), + action: async () => await commands.setSigner(pubkey), }), PredefinedMenuItem.new({ item: "Separator" }), MenuItem.new({ @@ -124,17 +129,28 @@ function Account({ pubkey }: { pubkey: string }) { }), PredefinedMenuItem.new({ item: "Separator" }), MenuItem.new({ - text: "Logout", + text: "Delete Account", action: async () => { const res = await commands.deleteAccount(pubkey); if (res.status === "ok") { + router.invalidate(); + + // Delete column associate with this account + appColumns.setState((prev) => + prev.filter((col) => + col.account ? col.account !== pubkey : col, + ), + ); + + // Check remain account const newAccounts = context.accounts.filter( (account) => account !== pubkey, ); + // Redirect to new account screen if (newAccounts.length < 1) { - navigate({ to: "/", replace: true }); + navigate({ to: "/new", replace: true }); } } }, diff --git a/src/routes/_app.tsx b/src/routes/_app.tsx new file mode 100644 index 00000000..8ecd558c --- /dev/null +++ b/src/routes/_app.tsx @@ -0,0 +1,14 @@ +import { commands } from '@/commands.gen' +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/_app')({ + beforeLoad: async () => { + const accounts = await commands.getAccounts() + + if (!accounts.length) { + throw redirect({ to: '/new', replace: true }) + } + + return { accounts } + }, +}) diff --git a/src/routes/_layout/index.lazy.tsx b/src/routes/_app/index.lazy.tsx similarity index 72% rename from src/routes/_layout/index.lazy.tsx rename to src/routes/_app/index.lazy.tsx index e8184d6f..61f6b2d6 100644 --- a/src/routes/_layout/index.lazy.tsx +++ b/src/routes/_app/index.lazy.tsx @@ -6,9 +6,7 @@ import { ArrowLeft, ArrowRight, Plus, StackPlus } from "@phosphor-icons/react"; import { createLazyFileRoute } from "@tanstack/react-router"; import { useStore } from "@tanstack/react-store"; import { listen } from "@tauri-apps/api/event"; -import { resolveResource } from "@tauri-apps/api/path"; import { getCurrentWindow } from "@tauri-apps/api/window"; -import { readTextFile } from "@tauri-apps/plugin-fs"; import useEmblaCarousel from "embla-carousel-react"; import { nanoid } from "nanoid"; import { @@ -21,12 +19,12 @@ import { import { createPortal } from "react-dom"; import { useDebouncedCallback } from "use-debounce"; -export const Route = createLazyFileRoute("/_layout/")({ +export const Route = createLazyFileRoute("/_app/")({ component: Screen, }); function Screen() { - const { accounts } = Route.useRouteContext(); + const initialAppColumns = Route.useLoaderData(); const columns = useStore(appColumns, (state) => state); const [emblaRef, emblaApi] = useEmblaCarousel({ @@ -43,7 +41,7 @@ function Screen() { }, [emblaApi]); const emitScrollEvent = useCallback(() => { - getCurrentWindow().emit("column_scroll", {}); + getCurrentWindow().emit("scrolling", {}); }, []); const add = useDebouncedCallback((column: LumeColumn) => { @@ -62,16 +60,18 @@ function Screen() { const move = useDebouncedCallback( (label: string, direction: "left" | "right") => { const newCols = [...columns]; + const existColumn = newCols.find((el) => el.label === label); - const col = newCols.find((el) => el.label === label); - const colIndex = newCols.findIndex((el) => el.label === label); + if (existColumn) { + const colIndex = newCols.findIndex((el) => el.label === label); - newCols.splice(colIndex, 1); + newCols.splice(colIndex, 1); - if (direction === "left") newCols.splice(colIndex - 1, 0, col); - if (direction === "right") newCols.splice(colIndex + 1, 0, col); + if (direction === "left") newCols.splice(colIndex - 1, 0, existColumn); + if (direction === "right") newCols.splice(colIndex + 1, 0, existColumn); - appColumns.setState(() => newCols); + appColumns.setState(() => newCols); + } }, 150, ); @@ -90,23 +90,6 @@ function Screen() { const reset = useDebouncedCallback(() => appColumns.setState(() => []), 150); - const handleKeyDown = useDebouncedCallback((event) => { - if (event.defaultPrevented) return; - - switch (event.code) { - case "ArrowLeft": - if (emblaApi) emblaApi.scrollPrev(); - break; - case "ArrowRight": - if (emblaApi) emblaApi.scrollNext(); - break; - default: - break; - } - - event.preventDefault(); - }, 150); - useEffect(() => { if (emblaApi) { emblaApi.on("scroll", emitScrollEvent); @@ -119,16 +102,6 @@ function Screen() { }; }, [emblaApi, emitScrollEvent]); - // Listen for keyboard event - useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [handleKeyDown]); - - // Listen for columns event useEffect(() => { const unlisten = listen("columns", (data) => { if (data.payload.type === "reset") reset(); @@ -146,31 +119,14 @@ function Screen() { }, []); useEffect(() => { - async function getSystemColumns() { - const systemPath = "resources/columns.json"; - const resourcePath = await resolveResource(systemPath); - const resourceFile = await readTextFile(resourcePath); - const cols: LumeColumn[] = JSON.parse(resourceFile); - - appColumns.setState(() => cols.filter((col) => col.default)); + if (initialAppColumns) { + appColumns.setState(() => initialAppColumns); } + }, [initialAppColumns]); - if (!columns.length) { - const prevColumns = window.localStorage.getItem("columns"); - - if (!prevColumns) { - getSystemColumns(); - } else { - const parsed: LumeColumn[] = JSON.parse(prevColumns); - const fil = parsed.filter((item) => - item.account ? accounts.includes(item.account) : item, - ); - appColumns.setState(() => fil); - } - } else { - window.localStorage.setItem("columns", JSON.stringify(columns)); - } - }, [columns.length]); + useEffect(() => { + window.localStorage.setItem("columns", JSON.stringify(columns)); + }, [columns]); return (
diff --git a/src/routes/_app/index.tsx b/src/routes/_app/index.tsx new file mode 100644 index 00000000..80954636 --- /dev/null +++ b/src/routes/_app/index.tsx @@ -0,0 +1,26 @@ +import type { LumeColumn } from '@/types' +import { createFileRoute } from '@tanstack/react-router' +import { resolveResource } from '@tauri-apps/api/path' +import { readTextFile } from '@tauri-apps/plugin-fs' + +export const Route = createFileRoute('/_app/')({ + loader: async ({ context }) => { + const prevColumns = window.localStorage.getItem('columns') + + if (!prevColumns) { + const resourcePath = await resolveResource('resources/columns.json') + const resourceFile = await readTextFile(resourcePath) + const content: LumeColumn[] = JSON.parse(resourceFile) + const initialAppColumns = content.filter((col) => col.default) + + return initialAppColumns + } else { + const parsed: LumeColumn[] = JSON.parse(prevColumns) + const initialAppColumns = parsed.filter((item) => + item.account ? context.accounts.includes(item.account) : item, + ) + + return initialAppColumns + } + }, +}) diff --git a/src/routes/_layout.tsx b/src/routes/_layout.tsx deleted file mode 100644 index 3dcb8189..00000000 --- a/src/routes/_layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { commands } from "@/commands.gen"; -import { createFileRoute, redirect } from "@tanstack/react-router"; - -export const Route = createFileRoute("/_layout")({ - beforeLoad: async () => { - const accounts = await commands.getAccounts(); - - if (!accounts.length) { - throw redirect({ to: "/new", replace: true }); - } - - return { accounts }; - }, -}); diff --git a/src/routes/_layout/index.tsx b/src/routes/_layout/index.tsx deleted file mode 100644 index 1cb6e3e7..00000000 --- a/src/routes/_layout/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/_layout/")(); diff --git a/src/routes/columns/_layout.tsx b/src/routes/columns/_layout.tsx index 2f2a24bc..da00cc76 100644 --- a/src/routes/columns/_layout.tsx +++ b/src/routes/columns/_layout.tsx @@ -1,8 +1,7 @@ -import { commands } from "@/commands.gen"; -import { appSettings } from "@/commons"; import { Outlet, createFileRoute } from "@tanstack/react-router"; export interface RouteSearch { + account?: string; label?: string; name?: string; redirect?: string; @@ -11,21 +10,11 @@ export interface RouteSearch { export const Route = createFileRoute("/columns/_layout")({ validateSearch: (search: Record): RouteSearch => { return { + account: search.account, label: search.label, name: search.name, }; }, - beforeLoad: async () => { - const res = await commands.getUserSettings(); - - if (res.status === "ok") { - appSettings.setState((state) => { - return { ...state, ...res.data }; - }); - } else { - throw new Error(res.error); - } - }, component: Layout, }); diff --git a/src/routes/auth/connect.lazy.tsx b/src/routes/new-account/connect.lazy.tsx similarity index 97% rename from src/routes/auth/connect.lazy.tsx rename to src/routes/new-account/connect.lazy.tsx index d25e0c67..f3a6ca28 100644 --- a/src/routes/auth/connect.lazy.tsx +++ b/src/routes/new-account/connect.lazy.tsx @@ -5,7 +5,7 @@ import { readText } from "@tauri-apps/plugin-clipboard-manager"; import { message } from "@tauri-apps/plugin-dialog"; import { useState, useTransition } from "react"; -export const Route = createLazyFileRoute("/auth/connect")({ +export const Route = createLazyFileRoute("/new-account/connect")({ component: Screen, }); diff --git a/src/routes/auth/import.lazy.tsx b/src/routes/new-account/import.lazy.tsx similarity index 98% rename from src/routes/auth/import.lazy.tsx rename to src/routes/new-account/import.lazy.tsx index f0259a66..59c5fef5 100644 --- a/src/routes/auth/import.lazy.tsx +++ b/src/routes/new-account/import.lazy.tsx @@ -6,7 +6,7 @@ import { readText } from "@tauri-apps/plugin-clipboard-manager"; import { message } from "@tauri-apps/plugin-dialog"; import { useState, useTransition } from "react"; -export const Route = createLazyFileRoute("/auth/import")({ +export const Route = createLazyFileRoute("/new-account/import")({ component: Screen, }); diff --git a/src/routes/auth/watch.lazy.tsx b/src/routes/new-account/watch.lazy.tsx similarity index 97% rename from src/routes/auth/watch.lazy.tsx rename to src/routes/new-account/watch.lazy.tsx index e701d936..0aa6ce8f 100644 --- a/src/routes/auth/watch.lazy.tsx +++ b/src/routes/new-account/watch.lazy.tsx @@ -6,7 +6,7 @@ import { readText } from "@tauri-apps/plugin-clipboard-manager"; import { message } from "@tauri-apps/plugin-dialog"; import { useState, useTransition } from "react"; -export const Route = createLazyFileRoute("/auth/watch")({ +export const Route = createLazyFileRoute("/new-account/watch")({ component: Screen, }); diff --git a/src/routes/new-post/index.lazy.tsx b/src/routes/new-post/index.lazy.tsx new file mode 100644 index 00000000..3c15fa7b --- /dev/null +++ b/src/routes/new-post/index.lazy.tsx @@ -0,0 +1,459 @@ +import { type Mention, type Result, commands } from "@/commands.gen"; +import { cn, displayNpub } from "@/commons"; +import { PublishIcon, Spinner } from "@/components"; +import { Note } from "@/components/note"; +import { User } from "@/components/user"; +import { useEvent } from "@/system"; +import type { Metadata } from "@/types"; +import { CaretDown } from "@phosphor-icons/react"; +import { createLazyFileRoute, useAwaited } from "@tanstack/react-router"; +import { Menu, MenuItem } from "@tauri-apps/api/menu"; +import { message } from "@tauri-apps/plugin-dialog"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + useTransition, +} from "react"; +import { createPortal } from "react-dom"; +import { + RichTextarea, + type RichTextareaHandle, + createRegexRenderer, +} from "rich-textarea"; +import { MediaButton } from "./-components/media"; +import { PowButton } from "./-components/pow"; +import { WarningButton } from "./-components/warning"; + +const MENTION_REG = /\B@([\-+\w]*)$/; +const MAX_LIST_LENGTH = 5; + +const renderer = createRegexRenderer([ + [ + /https?:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+/g, + ({ children, key, value }) => ( + + {children} + + ), + ], + [ + /(?:^|\W)nostr:(\w+)(?!\w)/g, + ({ children, key }) => ( + + {children} + + ), + ], + [ + /(?:^|\W)#(\w+)(?!\w)/g, + ({ children, key }) => ( + + {children} + + ), + ], +]); + +export const Route = createLazyFileRoute("/new-post/")({ + component: Screen, +}); + +function Screen() { + const { reply_to } = Route.useSearch(); + const { accounts, initialValue } = Route.useRouteContext(); + const { deferMentionList } = Route.useLoaderData(); + const users = useAwaited({ promise: deferMentionList })[0]; + + const [text, setText] = useState(""); + const [currentUser, setCurrentUser] = useState(null); + const [isPublish, setIsPublish] = useState(false); + const [error, setError] = useState(""); + const [isPending, startTransition] = useTransition(); + const [warning, setWarning] = useState({ enable: false, reason: "" }); + const [difficulty, setDifficulty] = useState({ enable: false, num: 21 }); + const [index, setIndex] = useState(0); + const [pos, setPos] = useState<{ + top: number; + left: number; + caret: number; + } | null>(null); + + const ref = useRef(null); + const targetText = pos ? text.slice(0, pos.caret) : text; + const match = pos && targetText.match(MENTION_REG); + const name = match?.[1] ?? ""; + const filtered = useMemo(() => { + if (!users?.length) return []; + return users + .filter((u) => u?.name?.toLowerCase().startsWith(name.toLowerCase())) + .slice(0, MAX_LIST_LENGTH); + }, [users, name]); + + const showContextMenu = useCallback(async (e: React.MouseEvent) => { + e.preventDefault(); + + const list: Promise[] = []; + + for (const account of accounts) { + const res = await commands.getProfile(account); + let name = "unknown"; + + if (res.status === "ok") { + const profile: Metadata = JSON.parse(res.data); + name = profile.display_name ?? profile.name ?? "unknown"; + } + + list.push( + MenuItem.new({ + text: `Publish as ${name} (${displayNpub(account, 16)})`, + action: async () => setCurrentUser(account), + }), + ); + } + + const items = await Promise.all(list); + const menu = await Menu.new({ items }); + + await menu.popup().catch((e) => console.error(e)); + }, []); + + const insert = (i: number) => { + if (!ref.current || !pos) return; + + const selected = filtered[i]; + + ref.current.setRangeText( + `nostr:${selected.pubkey} `, + pos.caret - name.length - 1, + pos.caret, + "end", + ); + + setPos(null); + setIndex(0); + }; + + const submit = () => { + startTransition(async () => { + if (!text.length) return; + if (!currentUser) return; + + const signer = await commands.hasSigner(currentUser); + + if (signer.status === "ok") { + if (!signer.data) { + const res = await commands.setSigner(currentUser); + + if (res.status === "error") { + await message(res.error, { kind: "error" }); + return; + } + } + + const content = text.trim(); + const warn = warning.enable ? warning.reason : null; + const diff = difficulty.enable ? difficulty.num : null; + + let res: Result; + + if (reply_to?.length) { + res = await commands.reply(content, reply_to, null); + } else { + res = await commands.publish(content, warn, diff); + } + + if (res.status === "ok") { + setText(""); + setIsPublish(true); + } else { + setError(res.error); + } + } + }); + }; + + useEffect(() => { + if (isPublish) { + const timer = setTimeout(() => setIsPublish((prev) => !prev), 3000); + + return () => { + clearTimeout(timer); + }; + } + }, [isPublish]); + + useEffect(() => { + if (initialValue?.length) { + setText(initialValue); + } + }, [initialValue]); + + useEffect(() => { + if (accounts?.length) { + setCurrentUser(accounts[0]); + } + }, [accounts]); + + return ( +
+
+
+ {reply_to?.length ? ( +
+ Reply to: + +
+ ) : error?.length ? ( +
+

{error}

+
+ ) : null} +
+ setText(e.target.value)} + onKeyDown={(e) => { + if (!pos || !filtered.length) return; + switch (e.code) { + case "ArrowUp": { + e.preventDefault(); + const nextIndex = + index <= 0 ? filtered.length - 1 : index - 1; + setIndex(nextIndex); + break; + } + case "ArrowDown": { + e.preventDefault(); + const prevIndex = + index >= filtered.length - 1 ? 0 : index + 1; + setIndex(prevIndex); + break; + } + case "Enter": + e.preventDefault(); + insert(index); + break; + case "Escape": + e.preventDefault(); + setPos(null); + setIndex(0); + break; + default: + break; + } + }} + onSelectionChange={(r) => { + if ( + r.focused && + MENTION_REG.test(text.slice(0, r.selectionStart)) + ) { + setPos({ + top: r.top + r.height, + left: r.left, + caret: r.selectionStart, + }); + setIndex(0); + } else { + setPos(null); + setIndex(0); + } + }} + disabled={isPending} + > + {renderer} + + {pos ? ( + createPortal( + , + document.body, + ) + ) : ( + <> + )} +
+
+ {warning.enable ? ( +
+ + Reason: + + + setWarning((prev) => ({ ...prev, reason: e.target.value })) + } + className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50" + /> +
+ ) : null} + {difficulty.enable ? ( +
+ + Difficulty: + + { + if (!/[0-9]/.test(event.key)) { + event.preventDefault(); + } + }} + placeholder="21" + defaultValue={difficulty.num} + onChange={(e) => + setWarning((prev) => ({ ...prev, num: Number(e.target.value) })) + } + className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50" + /> +
+ ) : null} +
+
+ + {currentUser ? ( + + ) : null} +
+
+ + + +
+
+
+ ); +} + +function MentionPopup({ + users, + index, + top, + left, + insert, +}: { + users: Mention[]; + index: number; + top: number; + left: number; + insert: (index: number) => void; +}) { + return ( +
+ {users.map((u, i) => ( +
{ + e.preventDefault(); + insert(i); + }} + > +
+ {u.avatar?.length ? ( + + ) : ( +
+ )} +
+ {u.name} +
+ ))} +
+ ); +} + +function EmbedNote({ id }: { id: string }) { + const { isLoading, isError, data } = useEvent(id); + + if (isLoading) { + return ; + } + + if (isError || !data) { + return
Event not found with your current relay set.
; + } + + return ( + + + + + + + +
{data.content}
+
+
+ ); +} diff --git a/src/routes/new-post/index.tsx b/src/routes/new-post/index.tsx index 69162bb1..b7f22e67 100644 --- a/src/routes/new-post/index.tsx +++ b/src/routes/new-post/index.tsx @@ -1,74 +1,13 @@ -import { type Mention, type Result, commands } from "@/commands.gen"; -import { cn, displayNpub } from "@/commons"; -import { PublishIcon, Spinner } from "@/components"; -import { Note } from "@/components/note"; -import { User } from "@/components/user"; -import { LumeWindow, useEvent } from "@/system"; -import type { Metadata } from "@/types"; -import { CaretDown } from "@phosphor-icons/react"; -import { createFileRoute } from "@tanstack/react-router"; -import { Menu, MenuItem } from "@tauri-apps/api/menu"; -import type { Window } from "@tauri-apps/api/window"; +import { type Mention, commands } from "@/commands.gen"; +import { createFileRoute, defer } from "@tanstack/react-router"; +import { invoke } from "@tauri-apps/api/core"; import { nip19 } from "nostr-tools"; -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, - useTransition, -} from "react"; -import { createPortal } from "react-dom"; -import { - RichTextarea, - type RichTextareaHandle, - createRegexRenderer, -} from "rich-textarea"; -import { MediaButton } from "./-components/media"; -import { PowButton } from "./-components/pow"; -import { WarningButton } from "./-components/warning"; type EditorSearch = { reply_to: string; quote: string; }; -const MENTION_REG = /\B@([\-+\w]*)$/; -const MAX_LIST_LENGTH = 5; - -const renderer = createRegexRenderer([ - [ - /https?:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+/g, - ({ children, key, value }) => ( - - {children} - - ), - ], - [ - /(?:^|\W)nostr:(\w+)(?!\w)/g, - ({ children, key }) => ( - - {children} - - ), - ], - [ - /(?:^|\W)#(\w+)(?!\w)/g, - ({ children, key }) => ( - - {children} - - ), - ], -]); - export const Route = createFileRoute("/new-post/")({ validateSearch: (search: Record): EditorSearch => { return { @@ -77,7 +16,6 @@ export const Route = createFileRoute("/new-post/")({ }; }, beforeLoad: async ({ search }) => { - let users: Mention[] = []; let initialValue: string; if (search?.quote?.length) { @@ -86,426 +24,12 @@ export const Route = createFileRoute("/new-post/")({ initialValue = ""; } - const res = await commands.getAllProfiles(); const accounts = await commands.getAccounts(); - if (res.status === "ok") { - users = res.data; - } - - return { accounts, users, initialValue }; + return { accounts, initialValue }; + }, + loader: async () => { + const query: Promise> = invoke("get_all_profiles"); + return { deferMentionList: defer(query) }; }, - component: Screen, }); - -function Screen() { - const { reply_to } = Route.useSearch(); - const { accounts, users, initialValue } = Route.useRouteContext(); - - const [text, setText] = useState(""); - const [currentUser, setCurrentUser] = useState(null); - const [popup, setPopup] = useState(null); - const [isPublish, setIsPublish] = useState(false); - const [error, setError] = useState(""); - const [isPending, startTransition] = useTransition(); - const [warning, setWarning] = useState({ enable: false, reason: "" }); - const [difficulty, setDifficulty] = useState({ enable: false, num: 21 }); - const [index, setIndex] = useState(0); - const [pos, setPos] = useState<{ - top: number; - left: number; - caret: number; - } | null>(null); - - const ref = useRef(null); - const targetText = pos ? text.slice(0, pos.caret) : text; - const match = pos && targetText.match(MENTION_REG); - const name = match?.[1] ?? ""; - const filtered = useMemo( - () => - users - .filter((u) => u.name.toLowerCase().startsWith(name.toLowerCase())) - .slice(0, MAX_LIST_LENGTH), - [name], - ); - - const showContextMenu = useCallback(async (e: React.MouseEvent) => { - e.preventDefault(); - - const list = []; - - for (const account of accounts) { - const res = await commands.getProfile(account); - let name = "unknown"; - - if (res.status === "ok") { - const profile: Metadata = JSON.parse(res.data); - name = profile.display_name ?? profile.name; - } - - list.push( - MenuItem.new({ - text: `Publish as ${name} (${displayNpub(account, 16)})`, - action: async () => setCurrentUser(account), - }), - ); - } - - const items = await Promise.all(list); - const menu = await Menu.new({ items }); - - await menu.popup().catch((e) => console.error(e)); - }, []); - - const insert = (i: number) => { - if (!ref.current || !pos) return; - - const selected = filtered[i]; - - ref.current.setRangeText( - `nostr:${selected.pubkey} `, - pos.caret - name.length - 1, - pos.caret, - "end", - ); - - setPos(null); - setIndex(0); - }; - - const publish = () => { - startTransition(async () => { - const content = text.trim(); - const warn = warning.enable ? warning.reason : undefined; - const diff = difficulty.enable ? difficulty.num : undefined; - - let res: Result; - - if (reply_to?.length) { - res = await commands.reply(content, reply_to, undefined); - } else { - res = await commands.publish(content, warn, diff); - } - - if (res.status === "ok") { - setText(""); - setIsPublish(true); - } else { - setError(res.error); - } - }); - }; - - const submit = async () => { - if (!text.length) { - return; - } - - if (currentUser) { - const signer = await commands.hasSigner(currentUser); - - if (signer.status === "ok") { - if (!signer.data) { - const newPopup = await LumeWindow.openPopup( - `/set-signer/${currentUser}`, - undefined, - false, - ); - - setPopup(newPopup); - return; - } - - publish(); - } - } - }; - - useEffect(() => { - if (!popup) return; - - const unlisten = popup.listen("signer-updated", () => { - publish(); - }); - - return () => { - unlisten.then((f) => f()); - }; - }, [popup]); - - useEffect(() => { - if (isPublish) { - const timer = setTimeout(() => setIsPublish((prev) => !prev), 5000); - - return () => { - clearTimeout(timer); - }; - } - }, [isPublish]); - - useEffect(() => { - if (initialValue?.length) { - setText(initialValue); - } - }, [initialValue]); - - useEffect(() => { - if (accounts?.length) { - setCurrentUser(accounts[0]); - } - }, [accounts]); - - return ( -
-
-
- {reply_to?.length ? ( -
- Reply to: - -
- ) : error?.length ? ( -
-

{error}

-
- ) : null} -
- setText(e.target.value)} - onKeyDown={(e) => { - if (!pos || !filtered.length) return; - switch (e.code) { - case "ArrowUp": { - e.preventDefault(); - const nextIndex = - index <= 0 ? filtered.length - 1 : index - 1; - setIndex(nextIndex); - break; - } - case "ArrowDown": { - e.preventDefault(); - const prevIndex = - index >= filtered.length - 1 ? 0 : index + 1; - setIndex(prevIndex); - break; - } - case "Enter": - e.preventDefault(); - insert(index); - break; - case "Escape": - e.preventDefault(); - setPos(null); - setIndex(0); - break; - default: - break; - } - }} - onSelectionChange={(r) => { - if ( - r.focused && - MENTION_REG.test(text.slice(0, r.selectionStart)) - ) { - setPos({ - top: r.top + r.height, - left: r.left, - caret: r.selectionStart, - }); - setIndex(0); - } else { - setPos(null); - setIndex(0); - } - }} - disabled={isPending} - > - {renderer} - - {pos ? ( - createPortal( - , - document.body, - ) - ) : ( - <> - )} -
-
- {warning.enable ? ( -
- - Reason: - - - setWarning((prev) => ({ ...prev, reason: e.target.value })) - } - className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50" - /> -
- ) : null} - {difficulty.enable ? ( -
- - Difficulty: - - { - if (!/[0-9]/.test(event.key)) { - event.preventDefault(); - } - }} - placeholder="21" - defaultValue={difficulty.num} - onChange={(e) => - setWarning((prev) => ({ ...prev, num: Number(e.target.value) })) - } - className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50" - /> -
- ) : null} -
-
- - {currentUser ? ( - - ) : null} -
-
- - - -
-
-
- ); -} - -function MentionPopup({ - users, - index, - top, - left, - insert, -}: { - users: Mention[]; - index: number; - top: number; - left: number; - insert: (index: number) => void; -}) { - return ( -
- {users.map((u, i) => ( -
{ - e.preventDefault(); - insert(i); - }} - > -
- {u.avatar?.length ? ( - - ) : ( -
- )} -
- {u.name} -
- ))} -
- ); -} - -function EmbedNote({ id }: { id: string }) { - const { isLoading, isError, data } = useEvent(id); - - if (isLoading) { - return ; - } - - if (isError || !data) { - return
Event not found with your current relay set.
; - } - - return ( - - - - - - - -
{data.content}
-
-
- ); -} diff --git a/src/routes/new.lazy.tsx b/src/routes/new.lazy.tsx index 0e98ca14..7b6d84dd 100644 --- a/src/routes/new.lazy.tsx +++ b/src/routes/new.lazy.tsx @@ -18,7 +18,7 @@ function Screen() {

Continue with Nostr Connect

@@ -28,7 +28,7 @@ function Screen() {

Continue with Secret Key

@@ -38,7 +38,7 @@ function Screen() {

diff --git a/src/routes/reset.lazy.tsx b/src/routes/reset.lazy.tsx deleted file mode 100644 index 52913923..00000000 --- a/src/routes/reset.lazy.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { commands } from "@/commands.gen"; -import { Frame, GoBack, Spinner } from "@/components"; -import { createLazyFileRoute } from "@tanstack/react-router"; -import { readText } from "@tauri-apps/plugin-clipboard-manager"; -import { message } from "@tauri-apps/plugin-dialog"; -import { useState, useTransition } from "react"; - -export const Route = createLazyFileRoute("/reset")({ - component: Screen, -}); - -function Screen() { - const navigate = Route.useNavigate(); - - const [key, setKey] = useState(""); - const [password, setPassword] = useState(""); - const [isPending, startTransition] = useTransition(); - - const pasteFromClipboard = async () => { - const val = await readText(); - setKey(val); - }; - - const submit = () => { - startTransition(async () => { - if (!key.startsWith("nsec1")) { - await message( - "You need to enter a valid private key starts with nsec", - { title: "Reset Password", kind: "info" }, - ); - return; - } - - if (!password.length) { - await message("You must set password to secure your key", { - title: "Reset Password", - kind: "info", - }); - return; - } - - const res = await commands.resetPassword(key, password); - - if (res.status === "ok") { - navigate({ to: "/", replace: true }); - } else { - await message(res.error, { - title: "Import Private Ket", - kind: "error", - }); - return; - } - }); - }; - - return ( -
-
-
-

- Reset password -

-
-
- -
- -
- setKey(e.target.value)} - className="pl-3 pr-12 rounded-lg w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600" - /> - -
-
-
- - setPassword(e.target.value)} - className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none" - /> -
- -
- - - Go back to previous screen - -
-
-
-
- ); -} diff --git a/src/routes/set-group.lazy.tsx b/src/routes/set-group.lazy.tsx index a632e1ba..aec9c195 100644 --- a/src/routes/set-group.lazy.tsx +++ b/src/routes/set-group.lazy.tsx @@ -40,6 +40,22 @@ function Screen() { const submit = () => { startTransition(async () => { + const signer = await commands.hasSigner(account); + + if (signer.status === "ok") { + if (!signer.data) { + const res = await commands.setSigner(account); + + if (res.status === "error") { + await message(res.error, { kind: "error" }); + return; + } + } + } else { + await message(signer.error, { kind: "error" }); + return; + } + const res = await commands.setGroup(title, null, null, users); if (res.status === "ok") { diff --git a/src/routes/set-interest.lazy.tsx b/src/routes/set-interest.lazy.tsx index 7964a8be..36dbb3b2 100644 --- a/src/routes/set-interest.lazy.tsx +++ b/src/routes/set-interest.lazy.tsx @@ -52,9 +52,26 @@ function Screen() { const submit = () => { startTransition(async () => { + const signer = await commands.hasSigner(account); + + if (signer.status === "ok") { + if (!signer.data) { + const res = await commands.setSigner(account); + + if (res.status === "error") { + await message(res.error, { kind: "error" }); + return; + } + } + } else { + await message(signer.error, { kind: "error" }); + return; + } + const content = hashtags.map((tag) => tag.toLowerCase().replace(" ", "-").replace("#", ""), ); + const res = await commands.setInterest(title, null, null, content); if (res.status === "ok") { diff --git a/src/routes/set-signer.$id.lazy.tsx b/src/routes/set-signer.$id.lazy.tsx deleted file mode 100644 index 70cab037..00000000 --- a/src/routes/set-signer.$id.lazy.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { commands } from "@/commands.gen"; -import { Spinner, User } from "@/components"; -import { Lock } from "@phosphor-icons/react"; -import { createLazyFileRoute } from "@tanstack/react-router"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { message } from "@tauri-apps/plugin-dialog"; -import { useState, useTransition } from "react"; - -export const Route = createLazyFileRoute("/set-signer/$id")({ - component: Screen, -}); - -function Screen() { - const { id } = Route.useParams(); - - const [password, setPassword] = useState(""); - const [isPending, startTransition] = useTransition(); - - const unlock = () => { - startTransition(async () => { - if (!password.length) { - await message("Password is required", { kind: "info" }); - return; - } - - const window = getCurrentWindow(); - const res = await commands.setSigner(id, password); - - if (res.status === "ok") { - await window.close(); - } else { - await message(res.error, { kind: "error" }); - return; - } - }); - }; - - return ( -
-
- - - - - - -
- setPassword(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") unlock(); - }} - disabled={isPending} - placeholder="Enter password to unlock" - className="px-3 w-full rounded-lg h-10 text-center bg-transparent border border-black/10 dark:border-white/10 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400" - /> - -
-
-
- -
-
- ); -} diff --git a/src/routes/zap.$id.lazy.tsx b/src/routes/zap.$id.lazy.tsx index 653cf801..a2edf49a 100644 --- a/src/routes/zap.$id.lazy.tsx +++ b/src/routes/zap.$id.lazy.tsx @@ -1,12 +1,11 @@ import { commands } from "@/commands.gen"; import { displayNpub } from "@/commons"; import { User } from "@/components"; -import { LumeWindow } from "@/system"; import type { Metadata } from "@/types"; import { CaretDown } from "@phosphor-icons/react"; import { createLazyFileRoute } from "@tanstack/react-router"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; -import { type Window, getCurrentWindow } from "@tauri-apps/api/window"; +import { getCurrentWindow } from "@tauri-apps/api/window"; import { message } from "@tauri-apps/plugin-dialog"; import { useCallback, useEffect, useState, useTransition } from "react"; import CurrencyInput from "react-currency-input-field"; @@ -20,8 +19,7 @@ export const Route = createLazyFileRoute("/zap/$id")({ function Screen() { const { accounts, event } = Route.useRouteContext(); - const [currentUser, setCurrentUser] = useState(null); - const [popup, setPopup] = useState(null); + const [currentUser, setCurrentUser] = useState(null); const [amount, setAmount] = useState(21); const [content, setContent] = useState(""); const [isCompleted, setIsCompleted] = useState(false); @@ -30,7 +28,7 @@ function Screen() { const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); - const list = []; + const list: Promise[] = []; for (const account of accounts) { const res = await commands.getProfile(account); @@ -38,7 +36,7 @@ function Screen() { if (res.status === "ok") { const profile: Metadata = JSON.parse(res.data); - name = profile.display_name ?? profile.name; + name = profile.display_name ?? profile.name ?? "unknown"; } list.push( @@ -57,51 +55,39 @@ function Screen() { const zap = () => { startTransition(async () => { - const res = await commands.zapEvent(event.id, amount.toString(), content); + if (!currentUser) return; - if (res.status === "ok") { - setIsCompleted(true); - // close current window - await getCurrentWindow().close(); - } else { - await message(res.error, { kind: "error" }); - return; - } - }); - }; - - const submit = async () => { - if (currentUser) { const signer = await commands.hasSigner(currentUser); if (signer.status === "ok") { if (!signer.data) { - const newPopup = await LumeWindow.openPopup( - `/set-signer/${currentUser}`, - undefined, - false, - ); + const res = await commands.setSigner(currentUser); - setPopup(newPopup); - return; + if (res.status === "error") { + await message(res.error, { kind: "error" }); + return; + } } - zap(); + const res = await commands.zapEvent( + event.id, + amount.toString(), + content, + ); + + if (res.status === "ok") { + setIsCompleted(true); + // close current window + await getCurrentWindow().close(); + } else { + await message(res.error, { kind: "error" }); + return; + } + } else { + return; } - } - }; - - useEffect(() => { - if (!popup) return; - - const unlisten = popup.listen("signer-updated", () => { - zap(); }); - - return () => { - unlisten.then((f) => f()); - }; - }, [popup]); + }; useEffect(() => { if (accounts?.length) { @@ -170,7 +156,7 @@ function Screen() {