Compare commits

..

13 Commits

Author SHA1 Message Date
490417771c chore: release 2024-11-07 15:02:05 +07:00
0cb491eaf9 feat: auto open browser if receive auth_url 2024-11-07 14:26:13 +07:00
ece6bcc125 feat: add DVM feeds 2024-11-07 09:26:28 +07:00
4b79e559d2 chore: clean up 2024-11-06 09:43:31 +07:00
322e510db2 fix: settings 2024-11-06 09:16:22 +07:00
4e279f127d chore: release 2024-11-06 07:45:00 +07:00
5655a8136d feat: improve dark mode and some copy 2024-11-05 19:33:47 +07:00
d80534c51f feat: allow user enter custom relay for relayfeeds 2024-11-05 15:04:39 +07:00
0b97248fb8 chore: release 2024-11-04 10:42:46 +07:00
f54f448ecb feat: add tray 2024-11-04 10:40:50 +07:00
bd1f2b899d refactor: sync based on interval not window event 2024-11-04 09:40:33 +07:00
efd3c83193 fix: missing check for update 2024-11-03 13:50:52 +07:00
85fa1e2359 fix: missing check for update 2024-11-03 13:50:31 +07:00
52 changed files with 1556 additions and 1208 deletions

View File

@@ -4,10 +4,7 @@
"enabled": true
},
"files": {
"ignore": [
"./src/routes.gen.ts",
"./src/commands.gen.ts"
]
"ignore": ["./src/routes.gen.ts", "./src/commands.gen.ts"]
},
"linter": {
"enabled": true,

79
src-tauri/Cargo.lock generated
View File

@@ -444,8 +444,9 @@ dependencies = [
[[package]]
name = "async-wsocket"
version = "0.9.0"
source = "git+https://github.com/shadowylab/async-wsocket?rev=4d6a5b1780e65dc657ac36e5990a97c10feef072#4d6a5b1780e65dc657ac36e5990a97c10feef072"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a107e3bdbe61e8e1e1341c57241b4b2d50501127b44bd2eff13b4635ab42d35a"
dependencies = [
"async-utility",
"futures",
@@ -2509,6 +2510,15 @@ dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "html5ever"
version = "0.26.0"
@@ -3479,8 +3489,8 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "nostr"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0"
version = "0.36.0"
source = "git+https://github.com/reyamir/nostr?branch=feat/open-auth-url#596ad460a6747ccc60c27dce8141bf2dd5765df6"
dependencies = [
"aes",
"async-trait",
@@ -3510,8 +3520,8 @@ dependencies = [
[[package]]
name = "nostr-connect"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0"
version = "0.36.0"
source = "git+https://github.com/reyamir/nostr?branch=feat/open-auth-url#596ad460a6747ccc60c27dce8141bf2dd5765df6"
dependencies = [
"async-trait",
"async-utility",
@@ -3520,12 +3530,13 @@ dependencies = [
"thiserror",
"tokio",
"tracing",
"webbrowser",
]
[[package]]
name = "nostr-database"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0"
version = "0.36.0"
source = "git+https://github.com/reyamir/nostr?branch=feat/open-auth-url#596ad460a6747ccc60c27dce8141bf2dd5765df6"
dependencies = [
"async-trait",
"flatbuffers",
@@ -3538,8 +3549,8 @@ dependencies = [
[[package]]
name = "nostr-lmdb"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0"
version = "0.36.0"
source = "git+https://github.com/reyamir/nostr?branch=feat/open-auth-url#596ad460a6747ccc60c27dce8141bf2dd5765df6"
dependencies = [
"heed",
"nostr",
@@ -3551,8 +3562,8 @@ dependencies = [
[[package]]
name = "nostr-relay-pool"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0"
version = "0.36.0"
source = "git+https://github.com/reyamir/nostr?branch=feat/open-auth-url#596ad460a6747ccc60c27dce8141bf2dd5765df6"
dependencies = [
"async-utility",
"async-wsocket",
@@ -3569,8 +3580,8 @@ dependencies = [
[[package]]
name = "nostr-sdk"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0"
version = "0.36.0"
source = "git+https://github.com/reyamir/nostr?branch=feat/open-auth-url#596ad460a6747ccc60c27dce8141bf2dd5765df6"
dependencies = [
"async-utility",
"atomic-destructor",
@@ -3588,12 +3599,13 @@ dependencies = [
[[package]]
name = "nostr-zapper"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0"
version = "0.36.0"
source = "git+https://github.com/reyamir/nostr?branch=feat/open-auth-url#596ad460a6747ccc60c27dce8141bf2dd5765df6"
dependencies = [
"async-trait",
"nostr",
"thiserror",
"webln",
]
[[package]]
@@ -3732,8 +3744,8 @@ dependencies = [
[[package]]
name = "nwc"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0"
version = "0.36.0"
source = "git+https://github.com/reyamir/nostr?branch=feat/open-auth-url#596ad460a6747ccc60c27dce8141bf2dd5765df6"
dependencies = [
"async-trait",
"async-utility",
@@ -7107,6 +7119,24 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webbrowser"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e5f07fb9bc8de2ddfe6b24a71a75430673fd679e568c48b52716cef1cfae923"
dependencies = [
"block2",
"core-foundation 0.10.0",
"home",
"jni",
"log",
"ndk-context",
"objc2",
"objc2-foundation",
"url",
"web-sys",
]
[[package]]
name = "webkit2gtk"
version = "2.0.1"
@@ -7151,6 +7181,19 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webln"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75257015c2a40fc43c672fb03b70311f75e48b1020c8acff808ca628c46d87c"
dependencies = [
"js-sys",
"secp256k1",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "webpki-roots"
version = "0.26.6"

View File

@@ -33,7 +33,7 @@ tauri-plugin-theme = "2.1.2"
tauri-plugin-decorum = { git = "https://github.com/clearlysid/tauri-plugin-decorum" }
tauri-specta = { version = "2.0.0-rc.15", features = ["derive", "typescript"] }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb"] }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb", "webln", "all-nips"] }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
specta = "^2.0.0-rc.20"
@@ -50,6 +50,10 @@ keyring = { version = "3", features = ["apple-native", "windows-native"] }
keyring-search = { git = "https://github.com/reyamir/keyring-search" }
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
[patch.'https://github.com/rust-nostr/nostr']
nostr-sdk = { git = "https://github.com/reyamir/nostr", branch = "feat/open-auth-url", features = ["lmdb", "webln", "all-nips"] }
nostr-connect = { git = "https://github.com/reyamir/nostr", branch = "feat/open-auth-url" }
[target.'cfg(target_os = "macos")'.dependencies]
border = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
share-picker = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }

View File

@@ -46,9 +46,7 @@
"decorum:allow-show-snap-overlay",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-message",
"dialog:default",
"process:allow-restart",
"process:allow-exit",
"fs:allow-read-file",

View File

@@ -1 +1 @@
{"window":{"identifier":"window","description":"Capability for the desktop","local":true,"windows":["*"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-close","core:window:allow-destroy","core:window:allow-set-focus","core:window:allow-center","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-set-webview-size","core:webview:allow-set-webview-position","core:webview:allow-webview-close","core:menu:allow-new","core:menu:allow-popup","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","shell:allow-open","store:default","prevent-default:default","theme:default",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["macOS","windows"]}}
{"window":{"identifier":"window","description":"Capability for the desktop","local":true,"windows":["*"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-close","core:window:allow-destroy","core:window:allow-set-focus","core:window:allow-center","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-set-webview-size","core:webview:allow-set-webview-position","core:webview:allow-webview-close","core:menu:allow-new","core:menu:allow-popup","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:default","process:allow-restart","process:allow-exit","fs:allow-read-file","shell:allow-open","store:default","prevent-default:default","theme:default",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["macOS","windows"]}}

BIN
src-tauri/icons/tray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

View File

@@ -239,6 +239,173 @@ pub async fn get_all_events_from(
Ok(alt_events)
}
#[tauri::command]
#[specta::specta]
pub async fn get_all_events_by_kind(
kind: u16,
until: Option<String>,
state: State<'_, Nostr>,
) -> Result<Vec<String>, 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()
.kind(Kind::Custom(kind))
.limit(FETCH_LIMIT)
.until(as_of);
let mut events = Events::new(&[filter.clone()]);
let mut rx = client
.stream_events(vec![filter], Some(Duration::from_secs(3)))
.await
.map_err(|e| e.to_string())?;
while let Some(event) = rx.next().await {
events.insert(event);
}
let alt_events: Vec<String> = events.iter().map(|ev| ev.as_json()).collect();
Ok(alt_events)
}
#[tauri::command]
#[specta::specta]
pub async fn get_all_providers(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client;
let filter = Filter::new()
.kind(Kind::Custom(31990))
.custom_tag(SingleLetterTag::lowercase(Alphabet::K), vec!["5300"]);
let mut events = Events::new(&[filter.clone()]);
let mut rx = client
.stream_events(vec![filter], Some(Duration::from_secs(3)))
.await
.map_err(|e| e.to_string())?;
while let Some(event) = rx.next().await {
events.insert(event);
}
let alt_events: Vec<String> = events.iter().map(|ev| ev.as_json()).collect();
Ok(alt_events)
}
#[tauri::command]
#[specta::specta]
pub async fn request_events_from_provider(
provider: String,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client;
let signer = client.signer().await.map_err(|err| err.to_string())?;
let public_key = signer
.get_public_key()
.await
.map_err(|err| err.to_string())?;
let provider = PublicKey::parse(&provider).map_err(|err| err.to_string())?;
// Get current user's relay list
let relay_list = client
.database()
.relay_list(public_key)
.await
.map_err(|err| err.to_string())?;
let relay_list: Vec<String> = relay_list.iter().map(|item| item.0.to_string()).collect();
// Create job request
let builder = EventBuilder::job_request(
Kind::JobRequest(5300),
vec![
Tag::public_key(provider),
Tag::custom(TagKind::Relays, relay_list),
],
)
.map_err(|err| err.to_string())?;
match client.send_event_builder(builder).await {
Ok(output) => {
let filter = Filter::new()
.kind(Kind::JobResult(6300))
.author(provider)
.pubkey(public_key)
.since(Timestamp::now());
let opts = SubscribeAutoCloseOptions::default()
.filter(FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(2)));
let _ = client.subscribe(vec![filter], Some(opts)).await;
Ok(output.val.to_hex())
}
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_all_events_by_request(
id: String,
provider: String,
state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let public_key = PublicKey::parse(&id).map_err(|err| err.to_string())?;
let provider = PublicKey::parse(&provider).map_err(|err| err.to_string())?;
let filter = Filter::new()
.kind(Kind::JobResult(6300))
.author(provider)
.pubkey(public_key)
.limit(1);
let events = client
.database()
.query(vec![filter])
.await
.map_err(|err| err.to_string())?;
if let Some(event) = events.first() {
let parsed: Vec<Vec<String>> =
serde_json::from_str(&event.content).map_err(|err| err.to_string())?;
let vec: Vec<Tag> = parsed
.into_iter()
.filter_map(|item| Tag::parse(&item).ok())
.collect::<Vec<_>>();
let tags = Tags::new(vec);
let ids: Vec<EventId> = tags.event_ids().copied().collect();
let filter = Filter::new().ids(ids);
let mut events = Events::new(&[filter.clone()]);
let mut rx = client
.stream_events(vec![filter], Some(Duration::from_secs(3)))
.await
.map_err(|e| e.to_string())?;
while let Some(event) = rx.next().await {
events.insert(event);
}
let alt_events = process_event(client, events, false).await;
Ok(alt_events)
} else {
Err("Job result not found.".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_local_events(

View File

@@ -381,6 +381,35 @@ pub async fn get_all_local_interests(
Ok(alt_events)
}
#[tauri::command]
#[specta::specta]
pub async fn get_relay_list(id: String, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
let filter = Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1);
let mut events = Events::new(&[filter.clone()]);
let mut rx = client
.stream_events(vec![filter], Some(Duration::from_secs(3)))
.await
.map_err(|e| e.to_string())?;
while let Some(event) = rx.next().await {
events.insert(event);
}
if let Some(event) = events.first() {
Ok(event.as_json())
} else {
Err("Relay list not found".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {

View File

@@ -2,5 +2,4 @@ pub mod account;
pub mod event;
pub mod metadata;
pub mod relay;
pub mod sync;
pub mod window;

View File

@@ -1,12 +1,8 @@
use nostr_sdk::prelude::*;
use serde::Serialize;
use specta::Type;
use std::{
fs::OpenOptions,
io::{self, BufRead, Write},
str::FromStr,
};
use tauri::{path::BaseDirectory, Manager, State};
use std::str::FromStr;
use tauri::State;
use crate::{Nostr, FETCH_LIMIT};
@@ -20,82 +16,17 @@ pub struct Relays {
#[tauri::command]
#[specta::specta]
pub async fn get_relays(id: String, state: State<'_, Nostr>) -> Result<Relays, String> {
pub async fn get_all_relays(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client;
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
let relays = client.pool().all_relays().await;
let v: Vec<String> = relays.iter().map(|item| item.0.to_string()).collect();
let connected_relays = client
.relays()
.await
.into_keys()
.map(|url| url.to_string())
.collect::<Vec<_>>();
let filter = Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1);
match client.database().query(vec![filter]).await {
Ok(events) => {
if let Some(event) = events.first() {
let nip65_list = nip65::extract_relay_list(event).collect::<Vec<_>>();
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 {
Ok(Relays {
connected: connected_relays,
read: None,
write: None,
both: None,
})
}
}
Err(e) => Err(e.to_string()),
}
Ok(v)
}
#[tauri::command]
#[specta::specta]
pub async fn get_all_relays(
pub async fn get_all_relay_lists(
until: Option<String>,
state: State<'_, Nostr>,
) -> Result<Vec<String>, String> {
@@ -149,36 +80,3 @@ pub async fn remove_relay(relay: String, state: State<'_, Nostr>) -> Result<(),
Ok(())
}
#[tauri::command]
#[specta::specta]
pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result<Vec<String>, 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);
reader
.lines()
.collect::<Result<Vec<String>, io::Error>>()
.map_err(|e| e.to_string())
}
#[tauri::command]
#[specta::specta]
pub fn set_bootstrap_relays(relays: String, app: tauri::AppHandle) -> Result<(), 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())?;
file.write_all(relays.as_bytes()).map_err(|e| e.to_string())
}

View File

@@ -1,116 +0,0 @@
use nostr_sdk::prelude::*;
use std::fs::{self, File};
use tauri::{ipc::Channel, Manager, State};
use crate::Nostr;
#[tauri::command]
#[specta::specta]
pub fn is_account_sync(id: String, app_handle: tauri::AppHandle) -> Result<bool, String> {
let config_dir = app_handle
.path()
.app_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 sync_account(
id: String,
state: State<'_, Nostr>,
reader: Channel<f64>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
let client = &state.client;
let public_key = PublicKey::from_bech32(&id).map_err(|e| e.to_string())?;
let filter = Filter::new().author(public_key).kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::RelayList,
Kind::MuteList,
Kind::EventDeletion,
Kind::Bookmarks,
Kind::BookmarkSet,
Kind::TextNote,
Kind::Repost,
Kind::Custom(30315),
]);
let (tx, mut rx) = SyncProgress::channel();
let opts = SyncOptions::default().progress(tx);
tauri::async_runtime::spawn(async move {
while (rx.changed().await).is_ok() {
let SyncProgress { total, current } = *rx.borrow_and_update();
if total > 0 {
reader
.send((current as f64 / total as f64) * 100.0)
.unwrap()
}
}
});
if let Ok(output) = client.sync(filter, &opts).await {
println!("Success: {:?}", output.success);
println!("Failed: {:?}", output.failed);
let event_pubkeys = client
.database()
.query(vec![Filter::new().kinds(vec![
Kind::ContactList,
Kind::FollowSet,
Kind::MuteList,
Kind::Repost,
Kind::TextNote,
])])
.await
.map_err(|e| e.to_string())?;
if !event_pubkeys.is_empty() {
let pubkeys: Vec<PublicKey> = event_pubkeys
.iter()
.flat_map(|ev| ev.tags.public_keys().copied())
.collect();
let filter = Filter::new()
.authors(pubkeys)
.kinds(vec![
Kind::Metadata,
Kind::TextNote,
Kind::Repost,
Kind::EventDeletion,
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::RelayList,
Kind::MuteList,
Kind::EventDeletion,
Kind::Bookmarks,
Kind::BookmarkSet,
Kind::Custom(30315),
])
.limit(10000);
if let Ok(output) = client.sync(filter, &opts).await {
println!("Success: {:?}", output.success);
println!("Failed: {:?}", output.failed);
}
};
}
let config_dir = app_handle
.path()
.app_config_dir()
.map_err(|e| e.to_string())?;
let _ = File::create(config_dir.join(id));
Ok(())
}

View File

@@ -71,11 +71,11 @@ pub async fn create_column(
if let Ok(public_key) = PublicKey::parse(&id) {
let is_newsfeed = payload.url().to_string().contains("newsfeed");
tauri::async_runtime::spawn(async move {
let state = webview.state::<Nostr>();
let client = &state.client;
if is_newsfeed {
tauri::async_runtime::spawn(async move {
let state = webview.state::<Nostr>();
let client = &state.client;
if is_newsfeed {
if let Ok(contact_list) =
client.database().contacts_public_keys(public_key).await
{
@@ -102,27 +102,31 @@ pub async fn create_column(
println!("Subscription error: {}", e);
}
}
}
});
});
}
} else if let Ok(event_id) = EventId::parse(&id) {
tauri::async_runtime::spawn(async move {
let state = webview.state::<Nostr>();
let client = &state.client;
let is_thread = payload.url().to_string().contains("events");
let subscription_id = SubscriptionId::new(webview.label());
if is_thread {
tauri::async_runtime::spawn(async move {
let state = webview.state::<Nostr>();
let client = &state.client;
let filter = Filter::new()
.event(event_id)
.kinds(vec![Kind::TextNote, Kind::Custom(1111)])
.since(Timestamp::now());
let subscription_id = SubscriptionId::new(webview.label());
if let Err(e) = client
.subscribe_with_id(subscription_id, vec![filter], None)
.await
{
println!("Subscription error: {}", e);
}
});
let filter = Filter::new()
.event(event_id)
.kinds(vec![Kind::TextNote, Kind::Custom(1111)])
.since(Timestamp::now());
if let Err(e) = client
.subscribe_with_id(subscription_id, vec![filter], None)
.await
{
println!("Subscription error: {}", e);
}
});
}
}
}
}

View File

@@ -11,16 +11,14 @@ use nostr_sdk::prelude::{Profile as DatabaseProfile, *};
use serde::{Deserialize, Serialize};
use specta::Type;
use specta_typescript::Typescript;
use std::{
collections::HashSet,
fs,
io::{self, BufRead},
str::FromStr,
time::Duration,
use std::{collections::HashSet, fs, str::FromStr, time::Duration};
use tauri::{
menu::{Menu, MenuItem},
Emitter, EventTarget, Listener, Manager, WebviewWindowBuilder,
};
use tauri::{path::BaseDirectory, Emitter, EventTarget, Listener, Manager};
use tauri_plugin_decorum::WebviewWindowExt;
use tauri_plugin_notification::{NotificationExt, PermissionState};
use tauri_plugin_store::StoreExt;
use tauri_specta::{collect_commands, Builder};
use tokio::{sync::RwLock, time::sleep};
@@ -30,7 +28,6 @@ pub mod common;
pub struct Nostr {
client: Client,
queue: RwLock<HashSet<PublicKey>>,
is_syncing: RwLock<bool>,
settings: RwLock<Settings>,
}
@@ -39,7 +36,7 @@ pub struct Payload {
id: String,
}
#[derive(Clone, Serialize, Deserialize, Type)]
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct Settings {
resize_service: bool,
content_warning: bool,
@@ -64,20 +61,26 @@ impl Default for Settings {
pub const DEFAULT_DIFFICULTY: u8 = 0;
pub const FETCH_LIMIT: usize = 50;
pub const QUEUE_DELAY: u64 = 300;
pub const QUEUE_DELAY: u64 = 150;
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
// Will be removed when almost relays support negentropy
pub const BOOTSTRAP_RELAYS: [&str; 5] = [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://nostr.fmt.wiz.biz",
"wss://directory.yabu.me",
"wss://purplepag.es",
];
fn main() {
tracing_subscriber::fmt::init();
let builder = Builder::<tauri::Wry>::new().commands(collect_commands![
get_relays,
get_all_relays,
get_all_relay_lists,
is_relay_connected,
connect_relay,
remove_relay,
get_bootstrap_relays,
set_bootstrap_relays,
get_accounts,
watch_account,
import_account,
@@ -102,6 +105,7 @@ fn main() {
get_interest,
get_all_interests,
get_all_local_interests,
get_relay_list,
set_wallet,
load_wallet,
remove_wallet,
@@ -117,6 +121,10 @@ fn main() {
get_all_events_by_authors,
get_all_events_by_hashtags,
get_all_events_from,
get_all_events_by_kind,
get_all_providers,
request_events_from_provider,
get_all_events_by_request,
get_local_events,
get_global_events,
search,
@@ -173,6 +181,53 @@ fn main() {
#[cfg(target_os = "macos")]
main_window.set_traffic_lights_inset(7.0, 10.0).unwrap();
// Setup tray menu item
let open_i = MenuItem::with_id(app, "open", "Open Lume", true, None::<&str>)?;
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
// Create tray menu
let menu = Menu::with_items(app, &[&open_i, &quit_i])?;
// Get main tray
let tray = app.tray_by_id("main").unwrap();
// Set menu
tray.set_menu(Some(menu)).unwrap();
// Listen to tray events
tray.on_menu_event(|handle, event| match event.id().as_ref() {
"open" => {
if let Some(window) = handle.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 = WebviewWindowBuilder::from_config(
handle,
handle.config().app.windows.first().unwrap(),
)
.unwrap()
.build()
.unwrap();
// Set decoration
#[cfg(target_os = "windows")]
window.create_overlay_titlebar().unwrap();
// Restore native border
#[cfg(target_os = "macos")]
window.add_border(None);
// Set a custom inset to the traffic lights
#[cfg(target_os = "macos")]
window.set_traffic_lights_inset(7.0, 10.0).unwrap();
}
}
"quit" => {
std::process::exit(0);
}
_ => {}
});
let client = tauri::async_runtime::block_on(async move {
// Setup database
let database = NostrLMDB::open(config_dir.join("nostr"))
@@ -181,8 +236,7 @@ fn main() {
// Config
let opts = Options::new()
.gossip(true)
.max_avg_latency(Duration::from_millis(300))
.automatic_authentication(true)
.max_avg_latency(Duration::from_millis(500))
.timeout(Duration::from_secs(5));
// Setup nostr client
@@ -191,7 +245,7 @@ fn main() {
.opts(opts)
.build();
// Get bootstrap relays
/* Get bootstrap relays
if let Ok(path) = handle
.path()
.resolve("resources/relays.txt", BaseDirectory::Resource)
@@ -218,6 +272,11 @@ fn main() {
}
}
}
*/
for relay in BOOTSTRAP_RELAYS {
let _ = client.add_relay(relay).await;
}
let _ = client.add_discovery_relay("wss://user.kindpag.es/").await;
@@ -227,112 +286,29 @@ fn main() {
client
});
// Load app settings
let store = app.store(".data")?;
// Parse app settings if exist
let settings = if let Some(data) = store.get("tanstack-query-[\"settings\"]") {
if let Some(str) = data.as_str() {
let v: Value = serde_json::from_str(str).unwrap();
let data = v["state"]["data"].clone();
let parse: Settings = serde_json::from_value(data).unwrap();
RwLock::new(parse)
} else {
RwLock::new(Settings::default())
}
} else {
RwLock::new(Settings::default())
};
// Create global state
app.manage(Nostr {
client,
settings,
queue: RwLock::new(HashSet::new()),
is_syncing: RwLock::new(false),
settings: RwLock::new(Settings::default()),
});
// Trigger some actions for window events
main_window.on_window_event(move |event| match event {
tauri::WindowEvent::Focused(focused) => {
if !focused {
let handle = handle_clone_event.clone();
tauri::async_runtime::spawn(async move {
let state = handle.state::<Nostr>();
let client = &state.client;
if *state.is_syncing.read().await {
return;
}
let mut is_syncing = state.is_syncing.write().await;
// Mark sync in progress
*is_syncing = true;
let opts = SyncOptions::default();
let accounts = get_all_accounts();
if !accounts.is_empty() {
let public_keys: Vec<PublicKey> = accounts
.iter()
.filter_map(|acc| PublicKey::from_str(acc).ok())
.collect();
let filter = Filter::new().pubkeys(public_keys).kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
]);
if let Ok(output) = client.sync(filter, &opts).await {
println!("Received: {}", output.received.len())
}
}
let filter = Filter::new().kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::ContactList,
Kind::FollowSet,
]);
// Get all public keys in database
if let Ok(events) = client.database().query(vec![filter]).await {
let public_keys: HashSet<PublicKey> = events
.iter()
.flat_map(|ev| ev.tags.public_keys().copied())
.collect();
let pk_vec: Vec<PublicKey> = public_keys.into_iter().collect();
for chunk in pk_vec.chunks(500) {
if chunk.is_empty() {
return;
}
let authors = chunk.to_owned();
let filter = Filter::new()
.authors(authors.clone())
.kinds(vec![
Kind::Metadata,
Kind::FollowSet,
Kind::Interests,
Kind::InterestSet,
])
.limit(1000);
if let Ok(output) = client.sync(filter, &opts).await {
println!("Received: {}", output.received.len())
}
let filter = Filter::new()
.authors(authors)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::EventDeletion,
])
.limit(500);
if let Ok(output) = client.sync(filter, &opts).await {
println!("Received: {}", output.received.len())
}
}
}
// Mark sync is done
*is_syncing = false;
});
}
}
tauri::WindowEvent::Moved(_size) => {}
_ => {}
});
// Listen for request metadata
@@ -379,7 +355,106 @@ fn main() {
});
});
// Run notification thread
// Run a thread for negentropy
tauri::async_runtime::spawn(async move {
let state = handle_clone_event.state::<Nostr>();
let client = &state.client;
// Use default sync options
let opts = SyncOptions::default();
// Set interval
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(600));
loop {
interval.tick().await;
let accounts = get_all_accounts();
let public_keys: Vec<PublicKey> = accounts
.iter()
.filter_map(|acc| PublicKey::from_str(acc).ok())
.collect();
if !public_keys.is_empty() {
// Create filter for notification
//
let filter = Filter::new().pubkeys(public_keys.clone()).kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
]);
// Sync notification
//
if let Ok(output) = client.sync_with(BOOTSTRAP_RELAYS, filter, &opts).await
{
println!("Received: {}", output.received.len())
}
// Create filter for contact list
//
let filter = Filter::new()
.authors(public_keys)
.kinds(vec![Kind::ContactList, Kind::FollowSet]);
// Sync events for contact list
//
if let Ok(events) = client.database().query(vec![filter]).await {
// Get unique public keys
let public_keys: HashSet<PublicKey> = events
.iter()
.flat_map(|ev| ev.tags.public_keys().copied())
.collect();
// Convert to vector
let public_keys: Vec<PublicKey> = public_keys.into_iter().collect();
for chunk in public_keys.chunks(1000) {
if chunk.is_empty() {
return;
}
let authors = chunk.to_owned();
// Create filter for metadata
//
let filter = Filter::new().authors(authors.clone()).kinds(vec![
Kind::Metadata,
Kind::FollowSet,
Kind::Interests,
Kind::InterestSet,
]);
// Sync metadata
//
if let Ok(output) =
client.sync_with(BOOTSTRAP_RELAYS, filter, &opts).await
{
println!("Received: {}", output.received.len())
}
// Create filter for text note
//
let filter = Filter::new()
.authors(authors)
.kinds(vec![Kind::TextNote, Kind::Repost, Kind::EventDeletion])
.limit(100);
// Sync text note
//
if let Ok(output) =
client.sync_with(BOOTSTRAP_RELAYS, filter, &opts).await
{
println!("Received: {}", output.received.len())
}
}
}
}
}
});
// Run a thread for handle notification
tauri::async_runtime::spawn(async move {
let state = handle_clone.state::<Nostr>();
let client = &state.client;
@@ -499,8 +574,13 @@ fn main() {
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_window_state::Builder::default().build())
.run(ctx)
.expect("error while running tauri application");
.build(ctx)
.expect("error while running tauri application")
.run(|_app_handle, event| {
if let tauri::RunEvent::ExitRequested { api, .. } = event {
api.prevent_exit();
}
});
}
#[cfg(debug_assertions)]

View File

@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Lume",
"version": "24.11.1",
"version": "24.11.5",
"identifier": "nu.lume.Lume",
"build": {
"beforeDevCommand": "pnpm dev",
@@ -30,6 +30,13 @@
"$RESOURCE/*"
]
}
},
"trayIcon": {
"id": "main",
"iconAsTemplate": true,
"menuOnLeftClick": true,
"tooltip": "Lume",
"iconPath": "./icons/tray.png"
}
},
"bundle": {
@@ -39,10 +46,7 @@
"targets": "all",
"active": true,
"category": "SocialNetworking",
"resources": [
"resources/*",
"locales/*"
],
"resources": ["resources/*", "locales/*"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",

View File

@@ -13,9 +13,7 @@
"hiddenTitle": true,
"transparent": true,
"windowEffects": {
"effects": [
"underWindowBackground"
]
"effects": ["underWindowBackground"]
}
}
]

View File

@@ -5,17 +5,17 @@
export const commands = {
async getRelays(id: string) : Promise<Result<Relays, string>> {
async getAllRelays() : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_relays", { id }) };
return { status: "ok", data: await TAURI_INVOKE("get_all_relays") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getAllRelays(until: string | null) : Promise<Result<string[], string>> {
async getAllRelayLists(until: string | null) : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_all_relays", { until }) };
return { status: "ok", data: await TAURI_INVOKE("get_all_relay_lists", { until }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -45,22 +45,6 @@ async removeRelay(relay: string) : Promise<Result<null, string>> {
else return { status: "error", error: e as any };
}
},
async getBootstrapRelays() : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_bootstrap_relays") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async setBootstrapRelays(relays: string) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("set_bootstrap_relays", { relays }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getAccounts() : Promise<string[]> {
return await TAURI_INVOKE("get_accounts");
},
@@ -248,6 +232,14 @@ async getAllLocalInterests(until: string | null) : Promise<Result<RichEvent[], s
else return { status: "error", error: e as any };
}
},
async getRelayList(id: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_relay_list", { id }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async setWallet(uri: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("set_wallet", { uri }) };
@@ -368,6 +360,38 @@ async getAllEventsFrom(url: string, until: string | null) : Promise<Result<RichE
else return { status: "error", error: e as any };
}
},
async getAllEventsByKind(kind: number, until: string | null) : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_all_events_by_kind", { kind, until }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getAllProviders() : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_all_providers") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async requestEventsFromProvider(provider: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("request_events_from_provider", { provider }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getAllEventsByRequest(id: string, provider: string) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_all_events_by_request", { id, provider }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getLocalEvents(until: string | null) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_local_events", { until }) };
@@ -528,7 +552,6 @@ export type Column = { label: string; url: string; x: number; y: number; width:
export type Mention = { pubkey: string; avatar: string; display_name: string; name: string }
export type Meta = { content: string; images: string[]; events: string[]; mentions: string[]; hashtags: string[] }
export type NewWindow = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean; closable: boolean }
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
export type RichEvent = { raw: string; parsed: Meta | null }
export type Settings = { resize_service: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean }

View File

@@ -3,11 +3,9 @@ import type {
MaybePromise,
PersistedQuery,
} from "@tanstack/query-persist-client-core";
import { ask, message, open } from "@tauri-apps/plugin-dialog";
import { open } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs";
import { relaunch } from "@tauri-apps/plugin-process";
import type { Store as TauriStore } from "@tauri-apps/plugin-store";
import { check } from "@tauri-apps/plugin-updater";
import { BitcoinUnit } from "bitcoin-units";
import { type ClassValue, clsx } from "clsx";
import dayjs from "dayjs";
@@ -23,6 +21,15 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function isValidRelayUrl(string: string) {
try {
const newUrl = new URL(string);
return newUrl.protocol === "ws:" || newUrl.protocol === "wss:";
} catch (err) {
return false;
}
}
export const isImagePath = (path: string) => {
const exts = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
@@ -170,41 +177,6 @@ export function decodeZapInvoice(tags: string[][]) {
}
}
export async function checkForAppUpdates(silent: boolean) {
const update = await check();
if (!update) {
if (silent) return;
await message("You are on the latest version. Stay awesome!", {
title: "No Update Available",
kind: "info",
okLabel: "OK",
});
return;
}
if (update?.available) {
const yes = await ask(
`Update to ${update.version} is available!\n\nRelease notes: ${update.body}`,
{
title: "Update Available",
kind: "info",
okLabel: "Update",
cancelLabel: "Cancel",
},
);
if (yes) {
await update.downloadAndInstall();
await relaunch();
}
return;
}
}
export async function upload(filePath?: string) {
const allowExts = [
"png",

View File

@@ -20,7 +20,7 @@ export function Column({ column }: { column: LumeColumn }) {
y: rect.y,
width: rect.width,
height: rect.height,
url: `${column.url}?label=${column.label}&name=${column.name}`,
url: `${column.url}?label=${column.label}&name=${column.name}&account=${column.account}`,
});
if (res.status === "error") {
@@ -160,7 +160,7 @@ function Header({
<button
type="button"
onClick={(e) => showContextMenu(e)}
className="hidden shrink-0 group-hover:inline-flex items-center justify-center size-6 border-[.5px] border-neutral-200 dark:border-neutral-800 shadow shadow-neutral-200/50 dark:shadow-none rounded-full bg-white dark:bg-black"
className="hidden shrink-0 group-hover:inline-flex items-center justify-center size-6 bg-white dark:bg-neutral-800 border-[.5px] border-neutral-200 dark:border-neutral-800 shadow shadow-neutral-200/50 dark:shadow-none rounded-full"
>
<CaretDown className="size-3" weight="bold" />
</button>

View File

@@ -16,7 +16,7 @@ export function NoteQuote({
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => LumeWindow.openEditor(null, event.id)}
onClick={() => LumeWindow.openEditor(undefined, event.id)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
label

View File

@@ -105,13 +105,11 @@ export function NoteRepost({
if (signer.status === "ok") {
if (!signer.data) {
if (!signer.data) {
const res = await commands.setSigner(account);
const res = await commands.setSigner(account);
if (res.status === "error") {
await message(res.error, { kind: "error" });
return;
}
if (res.status === "error") {
await message(res.error, { kind: "error" });
return;
}
}

View File

@@ -3,14 +3,12 @@ import { ZapIcon } from "@/components";
import { settingsQueryOptions } from "@/routes/__root";
import { LumeWindow } from "@/system";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useSearch } from "@tanstack/react-router";
import { useNoteContext } from "../provider";
export function NoteZap({
label = false,
smol = false,
}: { label?: boolean; smol?: boolean }) {
const search = useSearch({ strict: false });
const settings = useSuspenseQuery(settingsQueryOptions);
const event = useNoteContext();
@@ -19,7 +17,7 @@ export function NoteZap({
return (
<button
type="button"
onClick={() => LumeWindow.openZap(event.id, search.account)}
onClick={() => LumeWindow.openZap(event.id)}
className={cn(
"h-7 rounded-full inline-flex items-center justify-center text-neutral-800 hover:bg-black/5 dark:hover:bg-white/5 dark:text-neutral-200 text-sm font-medium",
label ? "w-24 gap-1.5" : "w-14",

View File

@@ -1,23 +1,28 @@
import { commands } from "@/commands.gen";
import { cn } from "@/commons";
import { cn, displayNpub } from "@/commons";
import { Spinner } from "@/components";
import { useQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { message } from "@tauri-apps/plugin-dialog";
import { useTransition } from "react";
import { useCallback, useTransition } from "react";
import { useUserContext } from "./provider";
import type { Metadata } from "@/types";
import { MenuItem, Menu } from "@tauri-apps/api/menu";
export function UserButton({ className }: { className?: string }) {
const user = useUserContext();
const queryClient = useQueryClient();
const { queryClient } = useRouteContext({ strict: false });
const {
isLoading,
isError,
data: isFollow,
} = useQuery({
queryKey: ["status", user.pubkey],
queryKey: ["status", user?.pubkey],
queryFn: async () => {
if (!user) {
throw new Error("User not found");
}
const res = await commands.isContact(user.pubkey);
if (res.status === "ok") {
@@ -27,28 +32,89 @@ export function UserButton({ className }: { className?: string }) {
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
retry: 2,
});
const [isPending, startTransition] = useTransition();
const toggleFollow = () => {
startTransition(async () => {
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const accounts = await commands.getAccounts();
const list: Promise<MenuItem>[] = [];
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 ?? "anon";
}
list.push(
MenuItem.new({
text: `Follow as ${name} (${displayNpub(account, 16)})`,
action: async () => submit(account),
}),
);
}
const items = await Promise.all(list);
const menu = await Menu.new({ items });
await menu.popup().catch((e) => console.error(e));
}, []);
const toggleFollow = useMutation({
mutationFn: async () => {
if (!user) return;
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ["status", user.pubkey] });
// Optimistically update to the new value
queryClient.setQueryData(
["status", user.pubkey],
(data: boolean) => !data,
);
const res = await commands.toggleContact(user.pubkey, null);
if (res.status === "ok") {
queryClient.setQueryData(
["status", user.pubkey],
(prev: boolean) => !prev,
);
// invalidate cache
await queryClient.invalidateQueries({
queryKey: ["status", user.pubkey],
});
return;
} else {
await message(res.error, { kind: "error" });
throw new Error(res.error);
}
},
onError: () => {
queryClient.setQueryData(["status", user?.pubkey], false);
},
onSettled: async () => {
return await queryClient.invalidateQueries({
queryKey: ["status", user?.pubkey],
});
},
});
const submit = (account: string) => {
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;
}
}
toggleFollow.mutate();
} else {
return;
}
});
@@ -57,8 +123,8 @@ export function UserButton({ className }: { className?: string }) {
return (
<button
type="button"
disabled={isPending}
onClick={() => toggleFollow()}
disabled={isPending || isLoading}
onClick={(e) => showContextMenu(e)}
className={cn("w-max gap-1", className)}
>
{isError ? "Error" : null}

View File

@@ -13,18 +13,17 @@ import { createFileRoute } from '@tanstack/react-router'
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
import { Route as AppImport } from './routes/_app'
import { Route as NewPostIndexImport } from './routes/new-post/index'
import { Route as AppIndexImport } from './routes/_app/index'
import { Route as ZapIdImport } from './routes/zap.$id'
import { Route as SettingsWalletImport } from './routes/settings/wallet'
import { Route as SettingsRelaysImport } from './routes/settings/relays'
import { Route as SettingsGeneralImport } from './routes/settings/general'
import { Route as ColumnsLayoutImport } from './routes/columns/_layout'
import { Route as IdSetProfileImport } from './routes/$id.set-profile'
import { Route as IdSetInterestImport } from './routes/$id.set-interest'
import { Route as IdSetGroupImport } from './routes/$id.set-group'
import { Route as SettingsIdWalletImport } from './routes/settings.$id/wallet'
import { Route as SettingsIdRelayImport } from './routes/settings.$id/relay'
import { Route as SettingsIdGeneralImport } from './routes/settings.$id/general'
import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global'
import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed'
import { Route as ColumnsLayoutStoriesIdImport } from './routes/columns/_layout/stories.$id'
@@ -38,8 +37,8 @@ import { Route as ColumnsLayoutCreateNewsfeedF2fImport } from './routes/columns/
// Create Virtual Routes
const ColumnsImport = createFileRoute('/columns')()
const SettingsLazyImport = createFileRoute('/settings')()
const NewLazyImport = createFileRoute('/new')()
const SettingsIdLazyImport = createFileRoute('/settings/$id')()
const NewAccountWatchLazyImport = createFileRoute('/new-account/watch')()
const NewAccountImportLazyImport = createFileRoute('/new-account/import')()
const NewAccountConnectLazyImport = createFileRoute('/new-account/connect')()
@@ -76,6 +75,9 @@ const ColumnsLayoutNotificationIdLazyImport = createFileRoute(
const ColumnsLayoutLaunchpadIdLazyImport = createFileRoute(
'/columns/_layout/launchpad/$id',
)()
const ColumnsLayoutDvmIdLazyImport = createFileRoute(
'/columns/_layout/dvm/$id',
)()
// Create/Update Routes
@@ -85,20 +87,18 @@ const ColumnsRoute = ColumnsImport.update({
getParentRoute: () => rootRoute,
} as any)
const SettingsLazyRoute = SettingsLazyImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/settings.lazy').then((d) => d.Route))
const NewLazyRoute = NewLazyImport.update({
id: '/new',
path: '/new',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
const BootstrapRelaysRoute = BootstrapRelaysImport.update({
id: '/bootstrap-relays',
path: '/bootstrap-relays',
getParentRoute: () => rootRoute,
} as any).lazy(() =>
import('./routes/bootstrap-relays.lazy').then((d) => d.Route),
)
const AppRoute = AppImport.update({
id: '/_app',
getParentRoute: () => rootRoute,
@@ -118,12 +118,6 @@ const AppIndexRoute = AppIndexImport.update({
getParentRoute: () => AppRoute,
} as any).lazy(() => import('./routes/_app/index.lazy').then((d) => d.Route))
const SettingsIdLazyRoute = SettingsIdLazyImport.update({
id: '/settings/$id',
path: '/settings/$id',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/settings.$id.lazy').then((d) => d.Route))
const NewAccountWatchLazyRoute = NewAccountWatchLazyImport.update({
id: '/new-account/watch',
path: '/new-account/watch',
@@ -154,6 +148,30 @@ const ZapIdRoute = ZapIdImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/zap.$id.lazy').then((d) => d.Route))
const SettingsWalletRoute = SettingsWalletImport.update({
id: '/wallet',
path: '/wallet',
getParentRoute: () => SettingsLazyRoute,
} as any).lazy(() =>
import('./routes/settings/wallet.lazy').then((d) => d.Route),
)
const SettingsRelaysRoute = SettingsRelaysImport.update({
id: '/relays',
path: '/relays',
getParentRoute: () => SettingsLazyRoute,
} as any).lazy(() =>
import('./routes/settings/relays.lazy').then((d) => d.Route),
)
const SettingsGeneralRoute = SettingsGeneralImport.update({
id: '/general',
path: '/general',
getParentRoute: () => SettingsLazyRoute,
} as any).lazy(() =>
import('./routes/settings/general.lazy').then((d) => d.Route),
)
const ColumnsLayoutRoute = ColumnsLayoutImport.update({
id: '/_layout',
getParentRoute: () => ColumnsRoute,
@@ -239,30 +257,6 @@ const ColumnsLayoutDiscoverInterestsLazyRoute =
),
)
const SettingsIdWalletRoute = SettingsIdWalletImport.update({
id: '/wallet',
path: '/wallet',
getParentRoute: () => SettingsIdLazyRoute,
} as any).lazy(() =>
import('./routes/settings.$id/wallet.lazy').then((d) => d.Route),
)
const SettingsIdRelayRoute = SettingsIdRelayImport.update({
id: '/relay',
path: '/relay',
getParentRoute: () => SettingsIdLazyRoute,
} as any).lazy(() =>
import('./routes/settings.$id/relay.lazy').then((d) => d.Route),
)
const SettingsIdGeneralRoute = SettingsIdGeneralImport.update({
id: '/general',
path: '/general',
getParentRoute: () => SettingsIdLazyRoute,
} as any).lazy(() =>
import('./routes/settings.$id/general.lazy').then((d) => d.Route),
)
const ColumnsLayoutGlobalRoute = ColumnsLayoutGlobalImport.update({
id: '/global',
path: '/global',
@@ -324,6 +318,14 @@ const ColumnsLayoutLaunchpadIdLazyRoute =
import('./routes/columns/_layout/launchpad.$id.lazy').then((d) => d.Route),
)
const ColumnsLayoutDvmIdLazyRoute = ColumnsLayoutDvmIdLazyImport.update({
id: '/dvm/$id',
path: '/dvm/$id',
getParentRoute: () => ColumnsLayoutRoute,
} as any).lazy(() =>
import('./routes/columns/_layout/dvm.$id.lazy').then((d) => d.Route),
)
const ColumnsLayoutStoriesIdRoute = ColumnsLayoutStoriesIdImport.update({
id: '/stories/$id',
path: '/stories/$id',
@@ -389,13 +391,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppImport
parentRoute: typeof rootRoute
}
'/bootstrap-relays': {
id: '/bootstrap-relays'
path: '/bootstrap-relays'
fullPath: '/bootstrap-relays'
preLoaderRoute: typeof BootstrapRelaysImport
parentRoute: typeof rootRoute
}
'/new': {
id: '/new'
path: '/new'
@@ -403,6 +398,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof NewLazyImport
parentRoute: typeof rootRoute
}
'/settings': {
id: '/settings'
path: '/settings'
fullPath: '/settings'
preLoaderRoute: typeof SettingsLazyImport
parentRoute: typeof rootRoute
}
'/$id/set-group': {
id: '/$id/set-group'
path: '/$id/set-group'
@@ -438,6 +440,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ColumnsLayoutImport
parentRoute: typeof ColumnsRoute
}
'/settings/general': {
id: '/settings/general'
path: '/general'
fullPath: '/settings/general'
preLoaderRoute: typeof SettingsGeneralImport
parentRoute: typeof SettingsLazyImport
}
'/settings/relays': {
id: '/settings/relays'
path: '/relays'
fullPath: '/settings/relays'
preLoaderRoute: typeof SettingsRelaysImport
parentRoute: typeof SettingsLazyImport
}
'/settings/wallet': {
id: '/settings/wallet'
path: '/wallet'
fullPath: '/settings/wallet'
preLoaderRoute: typeof SettingsWalletImport
parentRoute: typeof SettingsLazyImport
}
'/zap/$id': {
id: '/zap/$id'
path: '/zap/$id'
@@ -466,13 +489,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof NewAccountWatchLazyImport
parentRoute: typeof rootRoute
}
'/settings/$id': {
id: '/settings/$id'
path: '/settings/$id'
fullPath: '/settings/$id'
preLoaderRoute: typeof SettingsIdLazyImport
parentRoute: typeof rootRoute
}
'/_app/': {
id: '/_app/'
path: '/'
@@ -501,27 +517,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ColumnsLayoutGlobalImport
parentRoute: typeof ColumnsLayoutImport
}
'/settings/$id/general': {
id: '/settings/$id/general'
path: '/general'
fullPath: '/settings/$id/general'
preLoaderRoute: typeof SettingsIdGeneralImport
parentRoute: typeof SettingsIdLazyImport
}
'/settings/$id/relay': {
id: '/settings/$id/relay'
path: '/relay'
fullPath: '/settings/$id/relay'
preLoaderRoute: typeof SettingsIdRelayImport
parentRoute: typeof SettingsIdLazyImport
}
'/settings/$id/wallet': {
id: '/settings/$id/wallet'
path: '/wallet'
fullPath: '/settings/$id/wallet'
preLoaderRoute: typeof SettingsIdWalletImport
parentRoute: typeof SettingsIdLazyImport
}
'/columns/_layout/discover-interests': {
id: '/columns/_layout/discover-interests'
path: '/discover-interests'
@@ -613,6 +608,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ColumnsLayoutStoriesIdImport
parentRoute: typeof ColumnsLayoutImport
}
'/columns/_layout/dvm/$id': {
id: '/columns/_layout/dvm/$id'
path: '/dvm/$id'
fullPath: '/columns/dvm/$id'
preLoaderRoute: typeof ColumnsLayoutDvmIdLazyImport
parentRoute: typeof ColumnsLayoutImport
}
'/columns/_layout/launchpad/$id': {
id: '/columns/_layout/launchpad/$id'
path: '/launchpad/$id'
@@ -663,6 +665,22 @@ const AppRouteChildren: AppRouteChildren = {
const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
interface SettingsLazyRouteChildren {
SettingsGeneralRoute: typeof SettingsGeneralRoute
SettingsRelaysRoute: typeof SettingsRelaysRoute
SettingsWalletRoute: typeof SettingsWalletRoute
}
const SettingsLazyRouteChildren: SettingsLazyRouteChildren = {
SettingsGeneralRoute: SettingsGeneralRoute,
SettingsRelaysRoute: SettingsRelaysRoute,
SettingsWalletRoute: SettingsWalletRoute,
}
const SettingsLazyRouteWithChildren = SettingsLazyRoute._addFileChildren(
SettingsLazyRouteChildren,
)
interface ColumnsLayoutCreateNewsfeedRouteChildren {
ColumnsLayoutCreateNewsfeedF2fRoute: typeof ColumnsLayoutCreateNewsfeedF2fRoute
ColumnsLayoutCreateNewsfeedUsersRoute: typeof ColumnsLayoutCreateNewsfeedUsersRoute
@@ -694,6 +712,7 @@ interface ColumnsLayoutRouteChildren {
ColumnsLayoutInterestsIdRoute: typeof ColumnsLayoutInterestsIdRoute
ColumnsLayoutNewsfeedIdRoute: typeof ColumnsLayoutNewsfeedIdRoute
ColumnsLayoutStoriesIdRoute: typeof ColumnsLayoutStoriesIdRoute
ColumnsLayoutDvmIdLazyRoute: typeof ColumnsLayoutDvmIdLazyRoute
ColumnsLayoutLaunchpadIdLazyRoute: typeof ColumnsLayoutLaunchpadIdLazyRoute
ColumnsLayoutNotificationIdLazyRoute: typeof ColumnsLayoutNotificationIdLazyRoute
ColumnsLayoutRelaysUrlLazyRoute: typeof ColumnsLayoutRelaysUrlLazyRoute
@@ -718,6 +737,7 @@ const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = {
ColumnsLayoutInterestsIdRoute: ColumnsLayoutInterestsIdRoute,
ColumnsLayoutNewsfeedIdRoute: ColumnsLayoutNewsfeedIdRoute,
ColumnsLayoutStoriesIdRoute: ColumnsLayoutStoriesIdRoute,
ColumnsLayoutDvmIdLazyRoute: ColumnsLayoutDvmIdLazyRoute,
ColumnsLayoutLaunchpadIdLazyRoute: ColumnsLayoutLaunchpadIdLazyRoute,
ColumnsLayoutNotificationIdLazyRoute: ColumnsLayoutNotificationIdLazyRoute,
ColumnsLayoutRelaysUrlLazyRoute: ColumnsLayoutRelaysUrlLazyRoute,
@@ -740,42 +760,25 @@ const ColumnsRouteChildren: ColumnsRouteChildren = {
const ColumnsRouteWithChildren =
ColumnsRoute._addFileChildren(ColumnsRouteChildren)
interface SettingsIdLazyRouteChildren {
SettingsIdGeneralRoute: typeof SettingsIdGeneralRoute
SettingsIdRelayRoute: typeof SettingsIdRelayRoute
SettingsIdWalletRoute: typeof SettingsIdWalletRoute
}
const SettingsIdLazyRouteChildren: SettingsIdLazyRouteChildren = {
SettingsIdGeneralRoute: SettingsIdGeneralRoute,
SettingsIdRelayRoute: SettingsIdRelayRoute,
SettingsIdWalletRoute: SettingsIdWalletRoute,
}
const SettingsIdLazyRouteWithChildren = SettingsIdLazyRoute._addFileChildren(
SettingsIdLazyRouteChildren,
)
export interface FileRoutesByFullPath {
'': typeof AppRouteWithChildren
'/bootstrap-relays': typeof BootstrapRelaysRoute
'/new': typeof NewLazyRoute
'/settings': typeof SettingsLazyRouteWithChildren
'/$id/set-group': typeof IdSetGroupRoute
'/$id/set-interest': typeof IdSetInterestRoute
'/$id/set-profile': typeof IdSetProfileRoute
'/columns': typeof ColumnsLayoutRouteWithChildren
'/settings/general': typeof SettingsGeneralRoute
'/settings/relays': typeof SettingsRelaysRoute
'/settings/wallet': typeof SettingsWalletRoute
'/zap/$id': typeof ZapIdRoute
'/new-account/connect': typeof NewAccountConnectLazyRoute
'/new-account/import': typeof NewAccountImportLazyRoute
'/new-account/watch': typeof NewAccountWatchLazyRoute
'/settings/$id': typeof SettingsIdLazyRouteWithChildren
'/': typeof AppIndexRoute
'/new-post': typeof NewPostIndexRoute
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
'/columns/global': typeof ColumnsLayoutGlobalRoute
'/settings/$id/general': typeof SettingsIdGeneralRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute
'/settings/$id/wallet': typeof SettingsIdWalletRoute
'/columns/discover-interests': typeof ColumnsLayoutDiscoverInterestsLazyRoute
'/columns/discover-newsfeeds': typeof ColumnsLayoutDiscoverNewsfeedsLazyRoute
'/columns/discover-relays': typeof ColumnsLayoutDiscoverRelaysLazyRoute
@@ -789,6 +792,7 @@ export interface FileRoutesByFullPath {
'/columns/interests/$id': typeof ColumnsLayoutInterestsIdRoute
'/columns/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute
'/columns/stories/$id': typeof ColumnsLayoutStoriesIdRoute
'/columns/dvm/$id': typeof ColumnsLayoutDvmIdLazyRoute
'/columns/launchpad/$id': typeof ColumnsLayoutLaunchpadIdLazyRoute
'/columns/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute
'/columns/relays/$url': typeof ColumnsLayoutRelaysUrlLazyRoute
@@ -797,24 +801,23 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/bootstrap-relays': typeof BootstrapRelaysRoute
'/new': typeof NewLazyRoute
'/settings': typeof SettingsLazyRouteWithChildren
'/$id/set-group': typeof IdSetGroupRoute
'/$id/set-interest': typeof IdSetInterestRoute
'/$id/set-profile': typeof IdSetProfileRoute
'/columns': typeof ColumnsLayoutRouteWithChildren
'/settings/general': typeof SettingsGeneralRoute
'/settings/relays': typeof SettingsRelaysRoute
'/settings/wallet': typeof SettingsWalletRoute
'/zap/$id': typeof ZapIdRoute
'/new-account/connect': typeof NewAccountConnectLazyRoute
'/new-account/import': typeof NewAccountImportLazyRoute
'/new-account/watch': typeof NewAccountWatchLazyRoute
'/settings/$id': typeof SettingsIdLazyRouteWithChildren
'/': typeof AppIndexRoute
'/new-post': typeof NewPostIndexRoute
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
'/columns/global': typeof ColumnsLayoutGlobalRoute
'/settings/$id/general': typeof SettingsIdGeneralRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute
'/settings/$id/wallet': typeof SettingsIdWalletRoute
'/columns/discover-interests': typeof ColumnsLayoutDiscoverInterestsLazyRoute
'/columns/discover-newsfeeds': typeof ColumnsLayoutDiscoverNewsfeedsLazyRoute
'/columns/discover-relays': typeof ColumnsLayoutDiscoverRelaysLazyRoute
@@ -828,6 +831,7 @@ export interface FileRoutesByTo {
'/columns/interests/$id': typeof ColumnsLayoutInterestsIdRoute
'/columns/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute
'/columns/stories/$id': typeof ColumnsLayoutStoriesIdRoute
'/columns/dvm/$id': typeof ColumnsLayoutDvmIdLazyRoute
'/columns/launchpad/$id': typeof ColumnsLayoutLaunchpadIdLazyRoute
'/columns/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute
'/columns/relays/$url': typeof ColumnsLayoutRelaysUrlLazyRoute
@@ -838,25 +842,24 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRoute
'/_app': typeof AppRouteWithChildren
'/bootstrap-relays': typeof BootstrapRelaysRoute
'/new': typeof NewLazyRoute
'/settings': typeof SettingsLazyRouteWithChildren
'/$id/set-group': typeof IdSetGroupRoute
'/$id/set-interest': typeof IdSetInterestRoute
'/$id/set-profile': typeof IdSetProfileRoute
'/columns': typeof ColumnsRouteWithChildren
'/columns/_layout': typeof ColumnsLayoutRouteWithChildren
'/settings/general': typeof SettingsGeneralRoute
'/settings/relays': typeof SettingsRelaysRoute
'/settings/wallet': typeof SettingsWalletRoute
'/zap/$id': typeof ZapIdRoute
'/new-account/connect': typeof NewAccountConnectLazyRoute
'/new-account/import': typeof NewAccountImportLazyRoute
'/new-account/watch': typeof NewAccountWatchLazyRoute
'/settings/$id': typeof SettingsIdLazyRouteWithChildren
'/_app/': typeof AppIndexRoute
'/new-post/': typeof NewPostIndexRoute
'/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
'/settings/$id/general': typeof SettingsIdGeneralRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute
'/settings/$id/wallet': typeof SettingsIdWalletRoute
'/columns/_layout/discover-interests': typeof ColumnsLayoutDiscoverInterestsLazyRoute
'/columns/_layout/discover-newsfeeds': typeof ColumnsLayoutDiscoverNewsfeedsLazyRoute
'/columns/_layout/discover-relays': typeof ColumnsLayoutDiscoverRelaysLazyRoute
@@ -870,6 +873,7 @@ export interface FileRoutesById {
'/columns/_layout/interests/$id': typeof ColumnsLayoutInterestsIdRoute
'/columns/_layout/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute
'/columns/_layout/stories/$id': typeof ColumnsLayoutStoriesIdRoute
'/columns/_layout/dvm/$id': typeof ColumnsLayoutDvmIdLazyRoute
'/columns/_layout/launchpad/$id': typeof ColumnsLayoutLaunchpadIdLazyRoute
'/columns/_layout/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute
'/columns/_layout/relays/$url': typeof ColumnsLayoutRelaysUrlLazyRoute
@@ -881,24 +885,23 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| ''
| '/bootstrap-relays'
| '/new'
| '/settings'
| '/$id/set-group'
| '/$id/set-interest'
| '/$id/set-profile'
| '/columns'
| '/settings/general'
| '/settings/relays'
| '/settings/wallet'
| '/zap/$id'
| '/new-account/connect'
| '/new-account/import'
| '/new-account/watch'
| '/settings/$id'
| '/'
| '/new-post'
| '/columns/create-newsfeed'
| '/columns/global'
| '/settings/$id/general'
| '/settings/$id/relay'
| '/settings/$id/wallet'
| '/columns/discover-interests'
| '/columns/discover-newsfeeds'
| '/columns/discover-relays'
@@ -912,6 +915,7 @@ export interface FileRouteTypes {
| '/columns/interests/$id'
| '/columns/newsfeed/$id'
| '/columns/stories/$id'
| '/columns/dvm/$id'
| '/columns/launchpad/$id'
| '/columns/notification/$id'
| '/columns/relays/$url'
@@ -919,24 +923,23 @@ export interface FileRouteTypes {
| '/columns/users/$id'
fileRoutesByTo: FileRoutesByTo
to:
| '/bootstrap-relays'
| '/new'
| '/settings'
| '/$id/set-group'
| '/$id/set-interest'
| '/$id/set-profile'
| '/columns'
| '/settings/general'
| '/settings/relays'
| '/settings/wallet'
| '/zap/$id'
| '/new-account/connect'
| '/new-account/import'
| '/new-account/watch'
| '/settings/$id'
| '/'
| '/new-post'
| '/columns/create-newsfeed'
| '/columns/global'
| '/settings/$id/general'
| '/settings/$id/relay'
| '/settings/$id/wallet'
| '/columns/discover-interests'
| '/columns/discover-newsfeeds'
| '/columns/discover-relays'
@@ -950,6 +953,7 @@ export interface FileRouteTypes {
| '/columns/interests/$id'
| '/columns/newsfeed/$id'
| '/columns/stories/$id'
| '/columns/dvm/$id'
| '/columns/launchpad/$id'
| '/columns/notification/$id'
| '/columns/relays/$url'
@@ -958,25 +962,24 @@ export interface FileRouteTypes {
id:
| '__root__'
| '/_app'
| '/bootstrap-relays'
| '/new'
| '/settings'
| '/$id/set-group'
| '/$id/set-interest'
| '/$id/set-profile'
| '/columns'
| '/columns/_layout'
| '/settings/general'
| '/settings/relays'
| '/settings/wallet'
| '/zap/$id'
| '/new-account/connect'
| '/new-account/import'
| '/new-account/watch'
| '/settings/$id'
| '/_app/'
| '/new-post/'
| '/columns/_layout/create-newsfeed'
| '/columns/_layout/global'
| '/settings/$id/general'
| '/settings/$id/relay'
| '/settings/$id/wallet'
| '/columns/_layout/discover-interests'
| '/columns/_layout/discover-newsfeeds'
| '/columns/_layout/discover-relays'
@@ -990,6 +993,7 @@ export interface FileRouteTypes {
| '/columns/_layout/interests/$id'
| '/columns/_layout/newsfeed/$id'
| '/columns/_layout/stories/$id'
| '/columns/_layout/dvm/$id'
| '/columns/_layout/launchpad/$id'
| '/columns/_layout/notification/$id'
| '/columns/_layout/relays/$url'
@@ -1000,8 +1004,8 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
AppRoute: typeof AppRouteWithChildren
BootstrapRelaysRoute: typeof BootstrapRelaysRoute
NewLazyRoute: typeof NewLazyRoute
SettingsLazyRoute: typeof SettingsLazyRouteWithChildren
IdSetGroupRoute: typeof IdSetGroupRoute
IdSetInterestRoute: typeof IdSetInterestRoute
IdSetProfileRoute: typeof IdSetProfileRoute
@@ -1010,14 +1014,13 @@ export interface RootRouteChildren {
NewAccountConnectLazyRoute: typeof NewAccountConnectLazyRoute
NewAccountImportLazyRoute: typeof NewAccountImportLazyRoute
NewAccountWatchLazyRoute: typeof NewAccountWatchLazyRoute
SettingsIdLazyRoute: typeof SettingsIdLazyRouteWithChildren
NewPostIndexRoute: typeof NewPostIndexRoute
}
const rootRouteChildren: RootRouteChildren = {
AppRoute: AppRouteWithChildren,
BootstrapRelaysRoute: BootstrapRelaysRoute,
NewLazyRoute: NewLazyRoute,
SettingsLazyRoute: SettingsLazyRouteWithChildren,
IdSetGroupRoute: IdSetGroupRoute,
IdSetInterestRoute: IdSetInterestRoute,
IdSetProfileRoute: IdSetProfileRoute,
@@ -1026,7 +1029,6 @@ const rootRouteChildren: RootRouteChildren = {
NewAccountConnectLazyRoute: NewAccountConnectLazyRoute,
NewAccountImportLazyRoute: NewAccountImportLazyRoute,
NewAccountWatchLazyRoute: NewAccountWatchLazyRoute,
SettingsIdLazyRoute: SettingsIdLazyRouteWithChildren,
NewPostIndexRoute: NewPostIndexRoute,
}
@@ -1043,8 +1045,8 @@ export const routeTree = rootRoute
"filePath": "__root.tsx",
"children": [
"/_app",
"/bootstrap-relays",
"/new",
"/settings",
"/$id/set-group",
"/$id/set-interest",
"/$id/set-profile",
@@ -1053,7 +1055,6 @@ export const routeTree = rootRoute
"/new-account/connect",
"/new-account/import",
"/new-account/watch",
"/settings/$id",
"/new-post/"
]
},
@@ -1063,12 +1064,17 @@ export const routeTree = rootRoute
"/_app/"
]
},
"/bootstrap-relays": {
"filePath": "bootstrap-relays.tsx"
},
"/new": {
"filePath": "new.lazy.tsx"
},
"/settings": {
"filePath": "settings.lazy.tsx",
"children": [
"/settings/general",
"/settings/relays",
"/settings/wallet"
]
},
"/$id/set-group": {
"filePath": "$id.set-group.tsx"
},
@@ -1101,6 +1107,7 @@ export const routeTree = rootRoute
"/columns/_layout/interests/$id",
"/columns/_layout/newsfeed/$id",
"/columns/_layout/stories/$id",
"/columns/_layout/dvm/$id",
"/columns/_layout/launchpad/$id",
"/columns/_layout/notification/$id",
"/columns/_layout/relays/$url",
@@ -1108,6 +1115,18 @@ export const routeTree = rootRoute
"/columns/_layout/users/$id"
]
},
"/settings/general": {
"filePath": "settings/general.tsx",
"parent": "/settings"
},
"/settings/relays": {
"filePath": "settings/relays.tsx",
"parent": "/settings"
},
"/settings/wallet": {
"filePath": "settings/wallet.tsx",
"parent": "/settings"
},
"/zap/$id": {
"filePath": "zap.$id.tsx"
},
@@ -1120,14 +1139,6 @@ export const routeTree = rootRoute
"/new-account/watch": {
"filePath": "new-account/watch.lazy.tsx"
},
"/settings/$id": {
"filePath": "settings.$id.lazy.tsx",
"children": [
"/settings/$id/general",
"/settings/$id/relay",
"/settings/$id/wallet"
]
},
"/_app/": {
"filePath": "_app/index.tsx",
"parent": "/_app"
@@ -1147,18 +1158,6 @@ export const routeTree = rootRoute
"filePath": "columns/_layout/global.tsx",
"parent": "/columns/_layout"
},
"/settings/$id/general": {
"filePath": "settings.$id/general.tsx",
"parent": "/settings/$id"
},
"/settings/$id/relay": {
"filePath": "settings.$id/relay.tsx",
"parent": "/settings/$id"
},
"/settings/$id/wallet": {
"filePath": "settings.$id/wallet.tsx",
"parent": "/settings/$id"
},
"/columns/_layout/discover-interests": {
"filePath": "columns/_layout/discover-interests.lazy.tsx",
"parent": "/columns/_layout"
@@ -1211,6 +1210,10 @@ export const routeTree = rootRoute
"filePath": "columns/_layout/stories.$id.tsx",
"parent": "/columns/_layout"
},
"/columns/_layout/dvm/$id": {
"filePath": "columns/_layout/dvm.$id.lazy.tsx",
"parent": "/columns/_layout"
},
"/columns/_layout/launchpad/$id": {
"filePath": "columns/_layout/launchpad.$id.lazy.tsx",
"parent": "/columns/_layout"

View File

@@ -154,7 +154,7 @@ function Account({ pubkey }: { pubkey: string }) {
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({
text: "Settings",
action: () => LumeWindow.openSettings(pubkey),
action: () => LumeWindow.openSettings(),
}),
PredefinedMenuItem.new({ item: "Separator" }),
MenuItem.new({

View File

@@ -1,14 +1,41 @@
import { commands } from '@/commands.gen'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { commands } from "@/commands.gen";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { ask } from "@tauri-apps/plugin-dialog";
import { relaunch } from "@tauri-apps/plugin-process";
import { check } from "@tauri-apps/plugin-updater";
export const Route = createFileRoute('/_app')({
beforeLoad: async () => {
const accounts = await commands.getAccounts()
async function checkForAppUpdates() {
const update = await check();
if (!accounts.length) {
throw redirect({ to: '/new', replace: true })
}
if (update?.available) {
const yes = await ask(
`Update to ${update.version} is available!\n\nRelease notes: ${update.body}`,
{
title: "Update Available",
kind: "info",
okLabel: "Update",
cancelLabel: "Cancel",
},
);
return { accounts }
},
})
if (yes) {
await update.downloadAndInstall();
await relaunch();
}
return;
}
}
export const Route = createFileRoute("/_app")({
beforeLoad: async () => {
await checkForAppUpdates();
const accounts = await commands.getAccounts();
if (!accounts.length) {
throw redirect({ to: "/new", replace: true });
}
return { accounts };
},
});

View File

@@ -1,159 +0,0 @@
import { commands } from "@/commands.gen";
import { GoBack } from "@/components";
import { Frame } from "@/components/frame";
import { Spinner } from "@/components/spinner";
import { ArrowLeft, Plus, X } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { relaunch } from "@tauri-apps/plugin-process";
import { useEffect, useState, useTransition } from "react";
export const Route = createLazyFileRoute("/bootstrap-relays")({
component: Screen,
});
function Screen() {
const bootstrapRelays = Route.useLoaderData();
const [relays, setRelays] = useState<string[]>([]);
const [newRelay, setNewRelay] = useState("");
const [isPending, startTransition] = useTransition();
const add = () => {
try {
let url = newRelay;
if (!url.startsWith("wss://")) {
url = `wss://${url}`;
}
// Validate URL
const relay = new URL(url);
// Update
setRelays((prev) => [...prev, relay.toString()]);
setNewRelay("");
} catch {
message("URL is not valid.", { kind: "error" });
}
};
const remove = (relay: string) => {
setRelays((prev) => prev.filter((item) => item !== relay));
};
const submit = () => {
startTransition(async () => {
if (!relays.length) {
await message("You need to add at least 1 relay", {
title: "Manage Relays",
kind: "info",
});
return;
}
const merged = relays.join("\r\n");
const res = await commands.setBootstrapRelays(merged);
if (res.status === "ok") {
return await relaunch();
} else {
await message(res.error, {
title: "Manage Relays",
kind: "error",
});
return;
}
});
};
useEffect(() => {
setRelays(bootstrapRelays);
}, [bootstrapRelays]);
return (
<div
data-tauri-drag-region
className="relative size-full flex items-center justify-center"
>
<div className="w-[320px] flex flex-col gap-8">
<div className="flex flex-col gap-1 text-center">
<h1 className="leading-tight text-xl font-semibold">Manage Relays</h1>
<p className="text-sm text-neutral-700 dark:text-neutral-300">
The default relays that Lume will connected.
</p>
</div>
<div className="flex flex-col gap-3">
<Frame
className="flex flex-col gap-3 p-3 rounded-xl overflow-hidden"
shadow
>
<div className="flex gap-2">
<input
name="relay"
type="text"
placeholder="ex: relay.nostr.net, ..."
value={newRelay}
onChange={(e) => setNewRelay(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") add();
}}
className="flex-1 px-3 rounded-lg h-9 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
/>
<button
type="submit"
onClick={() => add()}
className="inline-flex items-center justify-center size-9 rounded-lg bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
<Plus className="size-5" />
</button>
</div>
<div className="flex flex-col gap-2">
{relays.map((relay) => (
<div
key={relay}
className="flex items-center justify-between h-9 px-2 rounded-lg bg-neutral-100 dark:bg-neutral-900"
>
<div className="text-sm font-medium">{relay}</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => remove(relay)}
className="inline-flex items-center justify-center rounded-md size-7 text-neutral-700 dark:text-white/20 hover:bg-black/10 dark:hover:bg-white/10"
>
<X className="size-3" />
</button>
</div>
</div>
))}
</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400">
<p>
Lume is heavily depend on Negentropy for syncing data. You need
to use at least 1 relay that support Negentropy. If you not
sure, you can keep using the default relay list.
</p>
</div>
</Frame>
<div className="flex flex-col items-center gap-1">
<button
type="button"
onClick={() => submit()}
disabled={isPending || !relays.length}
className="inline-flex items-center justify-center w-full h-9 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
>
{isPending ? <Spinner /> : "Save & Restart"}
</button>
<span className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
Lume will relaunch after saving.
</span>
</div>
</div>
</div>
<GoBack className="fixed top-11 left-2 flex items-center gap-1.5 text-sm font-medium">
<ArrowLeft className="size-5" />
Back
</GoBack>
</div>
);
}

View File

@@ -1,14 +0,0 @@
import { commands } from "@/commands.gen";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/bootstrap-relays")({
loader: async () => {
const res = await commands.getBootstrapRelays();
if (res.status === "ok") {
return res.data.map((item) => item.replace(",", ""));
} else {
throw new Error(res.error);
}
},
});

View File

@@ -1,16 +1,8 @@
import { cn } from "@/commons";
import type { ColumnRouteSearch } from "@/types";
import { Link, Outlet } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/columns/_layout/create-newsfeed")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
component: Screen,
});

View File

@@ -93,7 +93,7 @@ function Screen() {
data?.map((item) => (
<div
key={item}
className="w-full p-2 mb-2 overflow-hidden bg-white rounded-lg h-max dark:bg-black/20shadow-primary dark:ring-1 ring-neutral-800/50"
className="w-full p-2 mb-2 overflow-hidden bg-white rounded-lg h-max dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={item}>
<User.Root>

View File

@@ -143,12 +143,14 @@ function Screen() {
</p>
</div>
) : isError ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-sm text-center">{error?.message ?? "Error"}</p>
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
{error?.message ?? "Error"}
</p>
</div>
) : !data?.length ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-sm text-center">
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
Nothing to show yet, you can use Lume more and comeback lack to
see new events.
</p>
@@ -156,6 +158,13 @@ function Screen() {
) : (
data?.map((item) => renderItem(item))
)}
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
Lume running sync in the background,
<br />
the more you use the more event you see.
</p>
</div>
{hasNextPage ? (
<button
type="button"

View File

@@ -137,12 +137,14 @@ function Screen() {
</p>
</div>
) : isError ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-sm text-center">{error?.message ?? "Error"}</p>
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
{error?.message ?? "Error"}
</p>
</div>
) : !data?.length ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-sm text-center">
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
Nothing to show yet, you can use Lume more and comeback lack to
see new events.
</p>
@@ -150,6 +152,13 @@ function Screen() {
) : (
data?.map((item) => renderItem(item))
)}
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
Lume running sync in the background,
<br />
the more you use the more event you see.
</p>
</div>
{hasNextPage ? (
<button
type="button"

View File

@@ -27,7 +27,7 @@ function Screen() {
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const until = pageParam > 0 ? pageParam.toString() : null;
const res = await commands.getAllRelays(until);
const res = await commands.getAllRelayLists(until);
if (res.status === "ok") {
const data: NostrEvent[] = res.data.map((item) => JSON.parse(item));
@@ -115,12 +115,14 @@ function Screen() {
</p>
</div>
) : isError ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-sm text-center">{error?.message ?? "Error"}</p>
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
{error?.message ?? "Error"}
</p>
</div>
) : !data?.length ? (
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-sm text-center">
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
Nothing to show yet, you can use Lume more and comeback lack to
see new events.
</p>
@@ -128,6 +130,13 @@ function Screen() {
) : (
data?.map((item) => renderItem(item))
)}
<div className="mb-3 flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/20">
<p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
Lume running sync in the background,
<br />
the more you use the more event you see.
</p>
</div>
{hasNextPage ? (
<button
type="button"

View File

@@ -0,0 +1,108 @@
import { commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons";
import { RepostNote, Spinner, TextNote } from "@/components";
import type { LumeEvent } from "@/system";
import { Kind } from "@/types";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { type RefObject, useCallback, useRef } from "react";
import { Virtualizer } from "virtua";
export const Route = createLazyFileRoute("/columns/_layout/dvm/$id")({
component: Screen,
});
function Screen() {
const { id } = Route.useParams();
const { account } = Route.useSearch();
const { isLoading, isError, error, data } = useQuery({
queryKey: ["job-result", id],
queryFn: async () => {
if (!account) {
throw new Error("Account is required");
}
const res = await commands.getAllEventsByRequest(account, id);
if (res.status === "error") {
throw new Error(res.error);
}
return toLumeEvents(res.data);
},
refetchOnWindowFocus: false,
});
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback(
(event: LumeEvent) => {
if (!event) {
return;
}
switch (event.kind) {
case Kind.Repost: {
const repostId = event.repostId;
return (
<RepostNote
key={repostId + event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
}
default:
return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
}
},
[data],
);
return (
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full px-3"
>
<ScrollArea.Viewport
ref={ref}
className="relative h-full bg-white dark:bg-neutral-800 rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
{isLoading ? (
<div className="flex items-center justify-center w-full h-16 gap-2">
<Spinner className="size-4" />
<span className="text-sm font-medium">Requesting events...</span>
</div>
) : isError ? (
<div className="flex items-center justify-center w-full h-16 gap-2">
<span className="text-sm font-medium">{error?.message}</span>
</div>
) : !data?.length ? (
<div className="mb-3 flex items-center justify-center h-20 text-sm">
🎉 Yo. You're catching up on all latest notes.
</div>
) : (
data.map((item) => renderItem(item))
)}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
);
}

View File

@@ -175,7 +175,6 @@ function ReplyList() {
return events.filter((ev) => !removeQueues.has(ev.id));
},
refetchOnWindowFocus: false,
});
useEffect(() => {

View File

@@ -88,7 +88,7 @@ export function Screen() {
>
<ScrollArea.Viewport
ref={ref}
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
className="relative h-full bg-white dark:bg-neutral-800 rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
>
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
{isFetching && !isLoading && !isFetchingNextPage ? (

View File

@@ -1,5 +1,5 @@
import { commands } from "@/commands.gen";
import { cn, toLumeEvents } from "@/commons";
import { cn, isValidRelayUrl, toLumeEvents } from "@/commons";
import { Spinner, User } from "@/components";
import { LumeWindow } from "@/system";
import type { LumeColumn, NostrEvent } from "@/types";
@@ -8,9 +8,11 @@ import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path";
import { message } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { nanoid } from "nanoid";
import { useCallback } from "react";
import { memo, useCallback, useState, useTransition } from "react";
import { minidenticon } from "minidenticons";
export const Route = createLazyFileRoute("/columns/_layout/launchpad/$id")({
component: Screen,
@@ -25,7 +27,9 @@ function Screen() {
>
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
<Newsfeeds />
<Relayfeeds />
<Interests />
<ContentDiscovery />
<Core />
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
@@ -178,14 +182,14 @@ function Newsfeeds() {
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Newsfeeds",
name: "Browse Newsfeeds",
url: "/columns/discover-newsfeeds",
label: "discover_newsfeeds",
})
}
className="h-9 w-full px-3 flex items-center justify-between bg-neutral-200/50 hover:bg-neutral-200 rounded-lg dark:bg-neutral-800/50 dark:hover:bg-neutral-800"
>
<span className="text-xs font-medium">Discover newsfeeds</span>
<span className="text-xs font-medium">Browse newsfeeds</span>
<ArrowRight className="size-4" weight="bold" />
</button>
</div>
@@ -193,6 +197,179 @@ function Newsfeeds() {
);
}
function Relayfeeds() {
const { id } = Route.useParams();
const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({
queryKey: ["relays", id],
queryFn: async () => {
const res = await commands.getRelayList(id);
if (res.status === "ok") {
const event: NostrEvent = JSON.parse(res.data);
return event;
} else {
throw new Error(res.error);
}
},
refetchOnWindowFocus: false,
});
return (
<div className="mb-12 flex flex-col gap-3">
<div className="flex items-center justify-between px-2">
<h3 className="font-semibold">Relayfeeds</h3>
<div className="inline-flex items-center justify-center gap-2">
<button
type="button"
onClick={() => refetch()}
className={cn(
"size-7 inline-flex items-center justify-center rounded-full",
isRefetching ? "animate-spin" : "",
)}
>
<ArrowClockwise className="size-4" />
</button>
<button
type="button"
onClick={() => LumeWindow.openPopup(`${id}/set-group`, "New group")}
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
<Plus className="size-3" weight="bold" />
New
</button>
</div>
</div>
<div className="flex flex-col gap-3">
{isLoading ? (
<div className="inline-flex items-center gap-1.5">
<Spinner className="size-4" />
Loading...
</div>
) : isError ? (
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">{error?.message ?? "Error"}</p>
</div>
) : !data ? (
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">You don't have any relay list yet.</p>
</div>
) : (
<div className="flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800">
<div className="flex flex-col gap-2 p-2">
{data?.tags.map((tag) =>
tag[1]?.startsWith("wss://") ? (
<div
key={tag[1]}
className="group px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800"
>
<div className="flex-1 truncate select-text text-sm font-medium">
{tag[1]}
</div>
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
name: tag[1],
label: `relays_${tag[1].replace(/[^\w\s]/gi, "")}`,
url: `/columns/relays/${encodeURIComponent(tag[1])}`,
})
}
className="h-6 w-16 hidden group-hover:inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
) : null,
)}
</div>
<div className="p-2 flex items-center">
<User.Provider pubkey={data?.pubkey}>
<User.Root className="inline-flex items-center gap-2">
<User.Avatar className="size-7 rounded-full" />
<User.Name className="text-xs font-medium" />
</User.Root>
</User.Provider>
</div>
</div>
)}
<div className="flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800">
<RelayForm />
</div>
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Browse Relays",
url: "/columns/discover-relays",
label: "discover_relays",
})
}
className="h-9 w-full px-3 flex items-center justify-between bg-neutral-200/50 hover:bg-neutral-200 rounded-lg dark:bg-neutral-800/50 dark:hover:bg-neutral-800"
>
<span className="text-xs font-medium">Browse relays</span>
<ArrowRight className="size-4" weight="bold" />
</button>
</div>
</div>
);
}
function RelayForm() {
const [url, setUrl] = useState("");
const [isPending, startTransition] = useTransition();
const submit = () => {
startTransition(async () => {
if (!isValidRelayUrl(url)) {
await message("Relay URL is not valid", { kind: "error" });
return;
}
await LumeWindow.openColumn({
name: url,
label: `relays_${url.replace(/[^\w\s]/gi, "")}`,
url: `/columns/relays/${encodeURIComponent(url)}`,
});
setUrl("");
});
};
return (
<div className="flex flex-col gap-2 p-2">
<label
htmlFor="url"
className="text-xs font-semibold text-neutral-700 dark:text-neutral-300"
>
Add custom relay
</label>
<div className="flex gap-2">
<input
name="url"
type="url"
onChange={(e) => setUrl(e.currentTarget.value)}
onKeyDown={(event) => {
if (event.key === "Enter") submit();
}}
value={url}
disabled={isPending}
placeholder="wss://..."
spellCheck={false}
className="flex-1 px-3 bg-neutral-100 border-transparent rounded-lg h-9 dark:bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:placeholder:text-neutral-400"
/>
<button
type="button"
disabled={isPending}
onClick={() => submit()}
className="shrink-0 h-9 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-lg bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
</div>
);
}
function Interests() {
const { id } = Route.useParams();
const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({
@@ -261,22 +438,20 @@ function Interests() {
</User.Provider>
<h5 className="text-xs font-medium">{name}</h5>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
label,
name,
account: id,
url: `/columns/interests/${item.id}`,
})
}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
label,
name,
account: id,
url: `/columns/interests/${item.id}`,
})
}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
</div>
);
@@ -332,14 +507,14 @@ function Interests() {
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Interests",
name: "Browse Interests",
url: "/columns/discover-interests",
label: "discover_interests",
})
}
className="h-9 w-full px-3 flex items-center justify-between bg-neutral-200/50 hover:bg-neutral-200 rounded-lg dark:bg-neutral-800/50 dark:hover:bg-neutral-800"
>
<span className="text-xs font-medium">Discover interests</span>
<span className="text-xs font-medium">Browse interests</span>
<ArrowRight className="size-4" weight="bold" />
</button>
</div>
@@ -347,6 +522,132 @@ function Interests() {
);
}
function ContentDiscovery() {
const { isLoading, isError, error, data } = useQuery({
queryKey: ["content-discovery"],
queryFn: async () => {
const res = await commands.getAllProviders();
if (res.status === "ok") {
const events: NostrEvent[] = res.data.map((item) => JSON.parse(item));
return events;
} else {
throw new Error(res.error);
}
},
refetchOnWindowFocus: false,
});
return (
<div className="mb-12 flex flex-col gap-3">
<div className="flex items-center justify-between px-2">
<h3 className="font-semibold">Content Discovery</h3>
</div>
<div className="flex flex-col gap-3">
{isLoading ? (
<div className="inline-flex items-center gap-1.5">
<Spinner className="size-4" />
Loading...
</div>
) : isError ? (
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">{error?.message ?? "Error"}</p>
</div>
) : !data ? (
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">Empty.</p>
</div>
) : (
<div className="flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800">
<div className="flex flex-col gap-2 p-2">
{data?.map((item) => (
<Provider key={item.id} event={item} />
))}
</div>
</div>
)}
</div>
</div>
);
}
const Provider = memo(function Provider({ event }: { event: NostrEvent }) {
const { id } = Route.useParams();
const [isPending, startTransition] = useTransition();
const metadata: { [key: string]: string } = JSON.parse(event.content);
const fallback = `data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(event.id, 60, 50),
)}`;
const request = (name: string | undefined, provider: string) => {
startTransition(async () => {
// Ensure signer
const signer = await commands.hasSigner(id);
if (signer.status === "ok") {
if (!signer.data) {
const res = await commands.setSigner(id);
if (res.status === "error") {
await message(res.error, { kind: "error" });
return;
}
}
// Send request event to provider
const res = await commands.requestEventsFromProvider(provider);
if (res.status === "ok") {
// Open column
await LumeWindow.openColumn({
label: `dvm_${provider.slice(0, 6)}`,
name: name || "Content Discovery",
account: id,
url: `/columns/dvm/${provider}`,
});
return;
} else {
await message(res.error, { kind: "error" });
return;
}
} else {
await message(signer.error, { kind: "error" });
return;
}
});
};
return (
<div className="group px-3 flex gap-2 items-center justify-between h-16 rounded-lg bg-neutral-100 dark:bg-neutral-800">
<div className="shrink-0 size-10 bg-neutral-200 dark:bg-neutral-700 rounded-full overflow-hidden">
<img
src={metadata.picture || fallback}
alt={event.id}
className="size-10 object-cover"
/>
</div>
<div className="flex-1 flex flex-col truncate">
<h5 className="text-sm font-medium">{metadata.name}</h5>
<p className="w-full text-sm truncate text-neutral-600 dark:text-neutral-400">
{metadata.about}
</p>
</div>
<button
type="button"
onClick={() => request(metadata.name, event.pubkey)}
disabled={isPending}
className={cn(
"h-6 w-16 group-hover:visible inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white",
isPending ? "" : "invisible",
)}
>
{isPending ? <Spinner className="size-3" /> : "Add"}
</button>
</div>
);
});
function Core() {
const { id } = Route.useParams();
const { data } = useQuery({
@@ -391,22 +692,6 @@ function Core() {
Add
</button>
</div>
<div className="px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800">
<div className="text-sm font-medium">Relays</div>
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Relays",
label: "relays",
url: "/columns/discover-relays",
})
}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
{data?.map((column) => (
<div
key={column.label}

View File

@@ -27,32 +27,32 @@ function Screen() {
<div className="flex flex-col gap-4">
<a
href="/new-account/connect"
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-800 ring-1 ring-black/5 dark:ring-white/5"
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-600 ring-1 ring-black/5 dark:ring-white/5"
>
<h3 className="mb-1 font-medium">Continue with Nostr Connect</h3>
<p className="text-xs text-neutral-500 dark:text-neutral-500">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
Your account will be handled by a remote signer. Lume will not
store your account keys.
</p>
</a>
<a
href="/new-account/import"
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-800 ring-1 ring-black/5 dark:ring-white/5"
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-600 ring-1 ring-black/5 dark:ring-white/5"
>
<h3 className="mb-1 font-medium">Continue with Secret Key</h3>
<p className="text-xs text-neutral-500 dark:text-neutral-500">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
Lume will store your keys in secure storage. You can provide a
password to add extra security.
</p>
</a>
<a
href="/new-account/watch"
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-800 ring-1 ring-black/5 dark:ring-white/5"
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-600 ring-1 ring-black/5 dark:ring-white/5"
>
<h3 className="mb-1 font-medium">
Continue with Public Key (Watch Mode)
</h3>
<p className="text-xs text-neutral-500 dark:text-neutral-500">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
Use for experience without provide your private key, you can add
it later to publish new note.
</p>

View File

@@ -1,114 +0,0 @@
import { cn } from "@/commons";
import { CurrencyBtc, GearSix, HardDrives, User } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Link } from "@tanstack/react-router";
import { Outlet, createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/settings/$id")({
component: Screen,
});
function Screen() {
const { id } = Route.useParams();
const { platform } = Route.useRouteContext();
return (
<div className="flex size-full">
<div
data-tauri-drag-region
className={cn(
"w-[200px] shrink-0 flex flex-col gap-1 border-r border-black/10 dark:border-white/10 p-2",
platform === "macos" ? "pt-11" : "",
)}
>
<div className="h-8 px-1.5">
<h1 className="text-lg font-semibold">Settings</h1>
</div>
<Link to="/settings/$id/general" params={{ id }}>
{({ isActive }) => {
return (
<div
className={cn(
"h-9 w-full inline-flex items-center gap-1.5 rounded-lg p-2",
isActive
? "bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20"
: "text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
)}
>
<GearSix className="size-5 shrink-0" />
<p className="text-sm font-medium">General</p>
</div>
);
}}
</Link>
<Link to="/settings/$id/profile" params={{ id }}>
{({ isActive }) => {
return (
<div
className={cn(
"h-9 w-full inline-flex items-center gap-1.5 rounded-lg p-2",
isActive
? "bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20"
: "text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
)}
>
<User className="size-5 shrink-0" />
<p className="text-sm font-medium">Profile</p>
</div>
);
}}
</Link>
<Link to="/settings/$id/relay" params={{ id }}>
{({ isActive }) => {
return (
<div
className={cn(
"h-9 w-full inline-flex items-center gap-1.5 rounded-lg p-2",
isActive
? "bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20"
: "text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
)}
>
<HardDrives className="size-5 shrink-0" />
<p className="text-sm font-medium">Relay</p>
</div>
);
}}
</Link>
<Link to="/settings/$id/wallet" params={{ id }}>
{({ isActive }) => {
return (
<div
className={cn(
"h-9 w-full inline-flex items-center gap-1.5 rounded-lg p-2",
isActive
? "bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20"
: "text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
)}
>
<CurrencyBtc className="size-5 shrink-0" />
<p className="text-sm font-medium">Wallet</p>
</div>
);
}}
</Link>
</div>
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="flex-1 overflow-hidden size-full"
>
<ScrollArea.Viewport className="relative h-full pt-12">
<Outlet />
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
);
}

View File

@@ -1,3 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/settings/$id/general")();

View File

@@ -1,156 +0,0 @@
import { commands } from "@/commands.gen";
import { Plus, X } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState, useTransition } from "react";
export const Route = createLazyFileRoute("/settings/$id/relay")({
component: Screen,
});
function Screen() {
const { relayList } = Route.useRouteContext();
const [relays, setRelays] = useState<string[]>([]);
const [newRelay, setNewRelay] = useState<string>("");
const [isPending, startTransition] = useTransition();
const removeRelay = async (relay: string) => {
const res = await commands.removeRelay(relay);
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
};
const addNewRelay = () => {
startTransition(async () => {
try {
let url = newRelay;
if (!url.startsWith("wss://")) {
url = `wss://${url}`;
}
const relay = new URL(url);
const res = await commands.connectRelay(relay.toString());
if (res.status === "ok") {
setRelays((prev) => [...prev, newRelay]);
setNewRelay("");
} else {
await message(res.error, { title: "Relay", kind: "error" });
return;
}
} catch {
await message("URL is not valid.", { kind: "error" });
return;
}
});
};
useEffect(() => {
setRelays(relayList.connected);
}, [relayList]);
return (
<div className="w-full px-3 pb-3">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
Connected Relays
</h2>
<div className="flex flex-col px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
{relays.map((relay) => (
<div
key={relay}
className="flex items-center justify-between h-11"
>
<div className="inline-flex items-center gap-2 text-sm font-medium">
<span className="relative flex size-2">
<span className="absolute inline-flex w-full h-full bg-teal-400 rounded-full opacity-75 animate-ping" />
<span className="relative inline-flex bg-teal-500 rounded-full size-2" />
</span>
{relay}
</div>
<div>
<button
type="button"
onClick={() => removeRelay(relay)}
className="inline-flex items-center justify-center rounded-md size-7 hover:bg-black/10 dark:hover:bg-white/10"
>
<X className="size-4" />
</button>
</div>
</div>
))}
<div className="flex items-center h-14">
<div className="flex items-center w-full gap-2 mb-0">
<input
value={newRelay}
onChange={(e) => setNewRelay(e.target.value)}
name="url"
placeholder="wss://..."
disabled={isPending}
spellCheck={false}
className="flex-1 px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:border-neutral-700 dark:placeholder:text-neutral-400"
/>
<button
type="button"
disabled={isPending}
onClick={() => addNewRelay()}
className="inline-flex items-center justify-center w-16 px-2 text-sm font-medium text-white rounded-lg shrink-0 h-9 bg-black/20 dark:bg-white/20 hover:bg-blue-500 disabled:opacity-50"
>
<Plus className="size-5" />
</button>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<h2 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
User Relays (NIP-65)
</h2>
<div className="flex flex-col px-3 py-2 bg-black/5 dark:bg-white/5 rounded-xl">
<p className="text-sm text-yellow-500">
Lume will automatically connect to the user's relay list, but the
manager function (like adding, removing, changing relay purpose)
is not yet available.
</p>
</div>
<div className="flex flex-col px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
{relayList.read?.map((relay) => (
<div
key={relay}
className="flex items-center justify-between h-11"
>
<div className="text-sm font-medium">{relay}</div>
<div className="text-xs font-semibold">READ</div>
</div>
))}
{relayList.write?.map((relay) => (
<div
key={relay}
className="flex items-center justify-between h-11"
>
<div className="text-sm font-medium">{relay}</div>
<div className="text-xs font-semibold">WRITE</div>
</div>
))}
{relayList.both?.map((relay) => (
<div
key={relay}
className="flex items-center justify-between h-11"
>
<div className="text-sm font-medium">{relay}</div>
<div className="text-xs font-semibold">READ + WRITE</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,96 @@
import { cn } from '@/commons'
import { CurrencyBtc, GearSix, HardDrives } from '@phosphor-icons/react'
import * as ScrollArea from '@radix-ui/react-scroll-area'
import { Link } from '@tanstack/react-router'
import { Outlet, createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/settings')({
component: Screen,
})
function Screen() {
const { platform } = Route.useRouteContext()
return (
<div className="flex size-full">
<div
data-tauri-drag-region
className={cn(
'w-[200px] shrink-0 flex flex-col gap-1 border-r border-black/10 dark:border-white/10 p-2',
platform === 'macos' ? 'pt-11' : '',
)}
>
<div className="h-8 px-1.5">
<h1 className="text-lg font-semibold">Settings</h1>
</div>
<Link to="/settings/general">
{({ isActive }) => {
return (
<div
className={cn(
'h-9 w-full inline-flex items-center gap-1.5 rounded-lg p-2',
isActive
? 'bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20'
: 'text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10',
)}
>
<GearSix className="size-5 shrink-0" />
<p className="text-sm font-medium">General</p>
</div>
)
}}
</Link>
<Link to="/settings/relays">
{({ isActive }) => {
return (
<div
className={cn(
'h-9 w-full inline-flex items-center gap-1.5 rounded-lg p-2',
isActive
? 'bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20'
: 'text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10',
)}
>
<HardDrives className="size-5 shrink-0" />
<p className="text-sm font-medium">Relays</p>
</div>
)
}}
</Link>
<Link to="/settings/wallet">
{({ isActive }) => {
return (
<div
className={cn(
'h-9 w-full inline-flex items-center gap-1.5 rounded-lg p-2',
isActive
? 'bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20'
: 'text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10',
)}
>
<CurrencyBtc className="size-5 shrink-0" />
<p className="text-sm font-medium">Wallet</p>
</div>
)
}}
</Link>
</div>
<ScrollArea.Root
type={'scroll'}
scrollHideDelay={300}
className="flex-1 overflow-hidden size-full"
>
<ScrollArea.Viewport className="relative h-full pt-12">
<Outlet />
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div>
)
}

View File

@@ -17,7 +17,7 @@ import { settingsQueryOptions } from "../__root";
type Theme = "auto" | "light" | "dark";
export const Route = createLazyFileRoute("/settings/$id/general")({
export const Route = createLazyFileRoute("/settings/general")({
component: Screen,
});
@@ -46,6 +46,7 @@ function Screen() {
return;
} else {
await message(res.error, { kind: "error" });
return;
}
});
};

View File

@@ -0,0 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/settings/general")();

View File

@@ -0,0 +1,107 @@
import { commands } from "@/commands.gen";
import { isValidRelayUrl } from "@/commons";
import { Plus, X } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState, useTransition } from "react";
export const Route = createLazyFileRoute("/settings/relays")({
component: Screen,
});
function Screen() {
const { allRelays } = Route.useRouteContext();
const [relays, setRelays] = useState<string[]>([]);
const [newRelay, setNewRelay] = useState<string>("");
const [isPending, startTransition] = useTransition();
const removeRelay = async (relay: string) => {
const res = await commands.removeRelay(relay);
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
};
const addNewRelay = () => {
startTransition(async () => {
if (!isValidRelayUrl(newRelay)) {
await message("Relay URL is not valid", { kind: "error" });
return;
}
const res = await commands.connectRelay(newRelay);
if (res.status === "ok") {
setRelays((prev) => [...prev, newRelay]);
setNewRelay("");
} else {
await message(res.error, { title: "Relay", kind: "error" });
return;
}
});
};
useEffect(() => {
if (allRelays) setRelays(allRelays);
}, [allRelays]);
return (
<div className="w-full px-3 pb-3">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
Connected Relays
</h2>
<div className="flex flex-col px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center h-14">
<div className="flex items-center w-full gap-2 mb-0">
<input
value={newRelay}
onChange={(e) => setNewRelay(e.target.value)}
name="url"
placeholder="wss://..."
disabled={isPending}
spellCheck={false}
className="flex-1 px-3 bg-transparent rounded-lg h-9 border-neutral-400/50 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:border-neutral-800/50 dark:placeholder:text-neutral-400"
/>
<button
type="button"
disabled={isPending}
onClick={() => addNewRelay()}
className="inline-flex items-center justify-center w-16 px-2 text-sm font-medium text-white rounded-lg shrink-0 h-9 bg-black/20 dark:bg-white/20 hover:bg-blue-500 disabled:opacity-50"
>
<Plus className="size-5" />
</button>
</div>
</div>
{relays.map((relay) => (
<div
key={relay}
className="flex items-center justify-between h-11"
>
<div className="inline-flex items-center gap-2 text-sm font-medium truncate">
<span className="relative flex size-2">
<span className="absolute inline-flex w-full h-full bg-teal-400 rounded-full opacity-75 animate-ping" />
<span className="relative inline-flex bg-teal-500 rounded-full size-2" />
</span>
<span className="truncate">{relay}</span>
</div>
<button
type="button"
onClick={() => removeRelay(relay)}
className="inline-flex items-center justify-center rounded-md size-7 text-neutral-500 hover:bg-black/5 dark:hover:bg-white/5"
>
<X className="size-4" />
</button>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,12 +1,12 @@
import { commands } from "@/commands.gen";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/settings/$id/relay")({
beforeLoad: async ({ params }) => {
const res = await commands.getRelays(params.id);
export const Route = createFileRoute("/settings/relays")({
beforeLoad: async () => {
const res = await commands.getAllRelays();
if (res.status === "ok") {
return { relayList: res.data };
return { allRelays: res.data };
} else {
throw new Error(res.error);
}

View File

@@ -3,7 +3,7 @@ import { Button } from "@getalby/bitcoin-connect-react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createLazyFileRoute("/settings/$id/wallet")({
export const Route = createLazyFileRoute("/settings/wallet")({
component: Screen,
});

View File

@@ -1,7 +1,7 @@
import { init } from "@getalby/bitcoin-connect-react";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/settings/$id/wallet")({
export const Route = createFileRoute("/settings/wallet")({
beforeLoad: async () => {
init({
appName: "Lume",

View File

@@ -121,7 +121,7 @@ export const LumeWindow = {
throw new Error(query.error);
}
},
openZap: async (id: string, account?: string) => {
openZap: async (id: string) => {
const wallet = await commands.loadWallet();
if (wallet.status === "ok") {
@@ -136,16 +136,14 @@ export const LumeWindow = {
hidden_title: true,
closable: true,
});
} else if (account) {
await LumeWindow.openSettings(account, "wallet");
} else {
await LumeWindow.openSettings("wallet");
}
},
openSettings: async (account: string, path?: string) => {
openSettings: async (path?: string) => {
const query = await commands.openWindow({
label: "settings",
url: path
? `/settings/${account}/${path}`
: `/settings/${account}/general`,
url: path ? `/settings/${path}` : "/settings/general",
title: "Settings",
width: 700,
height: 500,

View File

@@ -1,10 +1,7 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"index.html",
],
content: ["./src/**/*.{js,ts,jsx,tsx}", "index.html"],
theme: {
extend: {
keyframes: {

BIN
tray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -2,19 +2,12 @@
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": [
"ESNext",
"ES2020",
"DOM",
"DOM.Iterable"
],
"lib": ["ESNext", "ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": "./",
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
},
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -26,11 +19,9 @@
"noUnusedLocals": true,
"noImplicitAny": false,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
],
"include": ["src"],
"references": [
{
"path": "./tsconfig.node.json"

View File

@@ -6,7 +6,5 @@
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
"include": ["vite.config.ts"]
}