Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e279f127d | ||
|
|
5655a8136d | ||
|
|
d80534c51f | ||
|
|
0b97248fb8 | ||
|
|
f54f448ecb | ||
|
|
bd1f2b899d | ||
|
|
efd3c83193 | ||
|
|
85fa1e2359 |
+1
-4
@@ -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,
|
||||
|
||||
Generated
+19
-18
@@ -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",
|
||||
@@ -3479,8 +3480,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/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"async-trait",
|
||||
@@ -3510,8 +3511,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/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"async-utility",
|
||||
@@ -3524,8 +3525,8 @@ dependencies = [
|
||||
|
||||
[[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/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"flatbuffers",
|
||||
@@ -3538,8 +3539,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/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
|
||||
dependencies = [
|
||||
"heed",
|
||||
"nostr",
|
||||
@@ -3551,8 +3552,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/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"async-wsocket",
|
||||
@@ -3569,8 +3570,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/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"atomic-destructor",
|
||||
@@ -3588,8 +3589,8 @@ 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/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"nostr",
|
||||
@@ -3732,8 +3733,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/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"async-utility",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]}}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 620 B |
@@ -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> {
|
||||
|
||||
+156
-107
@@ -18,7 +18,11 @@ use std::{
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::{path::BaseDirectory, Emitter, EventTarget, Listener, Manager};
|
||||
use tauri::{
|
||||
menu::{Menu, MenuItem},
|
||||
path::BaseDirectory,
|
||||
Emitter, EventTarget, Listener, Manager, WebviewWindowBuilder,
|
||||
};
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
use tauri_plugin_notification::{NotificationExt, PermissionState};
|
||||
use tauri_specta::{collect_commands, Builder};
|
||||
@@ -30,7 +34,6 @@ pub mod common;
|
||||
pub struct Nostr {
|
||||
client: Client,
|
||||
queue: RwLock<HashSet<PublicKey>>,
|
||||
is_syncing: RwLock<bool>,
|
||||
settings: RwLock<Settings>,
|
||||
}
|
||||
|
||||
@@ -64,7 +67,7 @@ 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";
|
||||
|
||||
fn main() {
|
||||
@@ -102,6 +105,7 @@ fn main() {
|
||||
get_interest,
|
||||
get_all_interests,
|
||||
get_all_local_interests,
|
||||
get_relay_list,
|
||||
set_wallet,
|
||||
load_wallet,
|
||||
remove_wallet,
|
||||
@@ -173,6 +177,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"))
|
||||
@@ -231,110 +282,9 @@ fn main() {
|
||||
app.manage(Nostr {
|
||||
client,
|
||||
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
|
||||
app.listen_any("request_metadata", move |event| {
|
||||
let payload = event.payload();
|
||||
@@ -379,7 +329,101 @@ 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(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(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(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 +543,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)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Lume",
|
||||
"version": "24.11.1",
|
||||
"version": "24.11.4",
|
||||
"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",
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
"hiddenTitle": true,
|
||||
"transparent": true,
|
||||
"windowEffects": {
|
||||
"effects": [
|
||||
"underWindowBackground"
|
||||
]
|
||||
"effects": ["underWindowBackground"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -248,6 +248,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 }) };
|
||||
|
||||
+10
-38
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,6 @@ 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'
|
||||
@@ -91,14 +90,6 @@ const NewLazyRoute = NewLazyImport.update({
|
||||
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,
|
||||
@@ -389,13 +380,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'
|
||||
@@ -758,7 +742,6 @@ const SettingsIdLazyRouteWithChildren = SettingsIdLazyRoute._addFileChildren(
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'': typeof AppRouteWithChildren
|
||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||
'/new': typeof NewLazyRoute
|
||||
'/$id/set-group': typeof IdSetGroupRoute
|
||||
'/$id/set-interest': typeof IdSetInterestRoute
|
||||
@@ -797,7 +780,6 @@ export interface FileRoutesByFullPath {
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||
'/new': typeof NewLazyRoute
|
||||
'/$id/set-group': typeof IdSetGroupRoute
|
||||
'/$id/set-interest': typeof IdSetInterestRoute
|
||||
@@ -838,7 +820,6 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/_app': typeof AppRouteWithChildren
|
||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||
'/new': typeof NewLazyRoute
|
||||
'/$id/set-group': typeof IdSetGroupRoute
|
||||
'/$id/set-interest': typeof IdSetInterestRoute
|
||||
@@ -881,7 +862,6 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| ''
|
||||
| '/bootstrap-relays'
|
||||
| '/new'
|
||||
| '/$id/set-group'
|
||||
| '/$id/set-interest'
|
||||
@@ -919,7 +899,6 @@ export interface FileRouteTypes {
|
||||
| '/columns/users/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/bootstrap-relays'
|
||||
| '/new'
|
||||
| '/$id/set-group'
|
||||
| '/$id/set-interest'
|
||||
@@ -958,7 +937,6 @@ export interface FileRouteTypes {
|
||||
id:
|
||||
| '__root__'
|
||||
| '/_app'
|
||||
| '/bootstrap-relays'
|
||||
| '/new'
|
||||
| '/$id/set-group'
|
||||
| '/$id/set-interest'
|
||||
@@ -1000,7 +978,6 @@ export interface FileRouteTypes {
|
||||
|
||||
export interface RootRouteChildren {
|
||||
AppRoute: typeof AppRouteWithChildren
|
||||
BootstrapRelaysRoute: typeof BootstrapRelaysRoute
|
||||
NewLazyRoute: typeof NewLazyRoute
|
||||
IdSetGroupRoute: typeof IdSetGroupRoute
|
||||
IdSetInterestRoute: typeof IdSetInterestRoute
|
||||
@@ -1016,7 +993,6 @@ export interface RootRouteChildren {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
AppRoute: AppRouteWithChildren,
|
||||
BootstrapRelaysRoute: BootstrapRelaysRoute,
|
||||
NewLazyRoute: NewLazyRoute,
|
||||
IdSetGroupRoute: IdSetGroupRoute,
|
||||
IdSetInterestRoute: IdSetInterestRoute,
|
||||
@@ -1043,7 +1019,6 @@ export const routeTree = rootRoute
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/_app",
|
||||
"/bootstrap-relays",
|
||||
"/new",
|
||||
"/$id/set-group",
|
||||
"/$id/set-interest",
|
||||
@@ -1063,9 +1038,6 @@ export const routeTree = rootRoute
|
||||
"/_app/"
|
||||
]
|
||||
},
|
||||
"/bootstrap-relays": {
|
||||
"filePath": "bootstrap-relays.tsx"
|
||||
},
|
||||
"/new": {
|
||||
"filePath": "new.lazy.tsx"
|
||||
},
|
||||
|
||||
+38
-11
@@ -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 };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -175,7 +175,6 @@ function ReplyList() {
|
||||
|
||||
return events.filter((ev) => !removeQueues.has(ev.id));
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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,10 @@ 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 { useCallback, useState, useTransition } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/launchpad/$id")({
|
||||
component: Screen,
|
||||
@@ -25,6 +26,7 @@ function Screen() {
|
||||
>
|
||||
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
|
||||
<Newsfeeds />
|
||||
<Relayfeeds />
|
||||
<Interests />
|
||||
<Core />
|
||||
</ScrollArea.Viewport>
|
||||
@@ -178,14 +180,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 +195,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({
|
||||
@@ -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>
|
||||
@@ -391,22 +566,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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-4
@@ -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: {
|
||||
|
||||
+4
-13
@@ -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"
|
||||
|
||||
+1
-3
@@ -6,7 +6,5 @@
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user