Compare commits

...
8 Commits
Author SHA1 Message Date
reya 4e279f127d chore: release 2024-11-06 07:45:00 +07:00
reya 5655a8136d feat: improve dark mode and some copy 2024-11-05 19:33:47 +07:00
reya d80534c51f feat: allow user enter custom relay for relayfeeds 2024-11-05 15:04:39 +07:00
reya 0b97248fb8 chore: release 2024-11-04 10:42:46 +07:00
reya f54f448ecb feat: add tray 2024-11-04 10:40:50 +07:00
reya bd1f2b899d refactor: sync based on interval not window event 2024-11-04 09:40:33 +07:00
reya efd3c83193 fix: missing check for update 2024-11-03 13:50:52 +07:00
reya 85fa1e2359 fix: missing check for update 2024-11-03 13:50:31 +07:00
29 changed files with 509 additions and 456 deletions
+1 -4
View File
@@ -4,10 +4,7 @@
"enabled": true "enabled": true
}, },
"files": { "files": {
"ignore": [ "ignore": ["./src/routes.gen.ts", "./src/commands.gen.ts"]
"./src/routes.gen.ts",
"./src/commands.gen.ts"
]
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
+19 -18
View File
@@ -444,8 +444,9 @@ dependencies = [
[[package]] [[package]]
name = "async-wsocket" name = "async-wsocket"
version = "0.9.0" version = "0.10.0"
source = "git+https://github.com/shadowylab/async-wsocket?rev=4d6a5b1780e65dc657ac36e5990a97c10feef072#4d6a5b1780e65dc657ac36e5990a97c10feef072" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a107e3bdbe61e8e1e1341c57241b4b2d50501127b44bd2eff13b4635ab42d35a"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"futures", "futures",
@@ -3479,8 +3480,8 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.35.0" version = "0.36.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0" source = "git+https://github.com/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
dependencies = [ dependencies = [
"aes", "aes",
"async-trait", "async-trait",
@@ -3510,8 +3511,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.35.0" version = "0.36.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0" source = "git+https://github.com/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"async-utility", "async-utility",
@@ -3524,8 +3525,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.35.0" version = "0.36.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0" source = "git+https://github.com/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"flatbuffers", "flatbuffers",
@@ -3538,8 +3539,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.35.0" version = "0.36.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0" source = "git+https://github.com/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
dependencies = [ dependencies = [
"heed", "heed",
"nostr", "nostr",
@@ -3551,8 +3552,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.35.0" version = "0.36.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0" source = "git+https://github.com/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3569,8 +3570,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.35.0" version = "0.36.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0" source = "git+https://github.com/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"atomic-destructor", "atomic-destructor",
@@ -3588,8 +3589,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-zapper" name = "nostr-zapper"
version = "0.35.0" version = "0.36.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0" source = "git+https://github.com/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"nostr", "nostr",
@@ -3732,8 +3733,8 @@ dependencies = [
[[package]] [[package]]
name = "nwc" name = "nwc"
version = "0.35.0" version = "0.36.0"
source = "git+https://github.com/rust-nostr/nostr#497c72f5a255c3d0cdf2a837e85c24be3d162fc0" source = "git+https://github.com/rust-nostr/nostr#33fb0699baf3a98b22b67ee6a86317a22cac927c"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"async-utility", "async-utility",
+1 -3
View File
@@ -46,9 +46,7 @@
"decorum:allow-show-snap-overlay", "decorum:allow-show-snap-overlay",
"clipboard-manager:allow-write-text", "clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text", "clipboard-manager:allow-read-text",
"dialog:allow-open", "dialog:default",
"dialog:allow-ask",
"dialog:allow-message",
"process:allow-restart", "process:allow-restart",
"process:allow-exit", "process:allow-exit",
"fs:allow-read-file", "fs:allow-read-file",
+1 -1
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"]}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

+29
View File
@@ -381,6 +381,35 @@ pub async fn get_all_local_interests(
Ok(alt_events) 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] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> { pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
+156 -107
View File
@@ -18,7 +18,11 @@ use std::{
str::FromStr, str::FromStr,
time::Duration, 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_decorum::WebviewWindowExt;
use tauri_plugin_notification::{NotificationExt, PermissionState}; use tauri_plugin_notification::{NotificationExt, PermissionState};
use tauri_specta::{collect_commands, Builder}; use tauri_specta::{collect_commands, Builder};
@@ -30,7 +34,6 @@ pub mod common;
pub struct Nostr { pub struct Nostr {
client: Client, client: Client,
queue: RwLock<HashSet<PublicKey>>, queue: RwLock<HashSet<PublicKey>>,
is_syncing: RwLock<bool>,
settings: RwLock<Settings>, settings: RwLock<Settings>,
} }
@@ -64,7 +67,7 @@ impl Default for Settings {
pub const DEFAULT_DIFFICULTY: u8 = 0; pub const DEFAULT_DIFFICULTY: u8 = 0;
pub const FETCH_LIMIT: usize = 50; 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"; pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
fn main() { fn main() {
@@ -102,6 +105,7 @@ fn main() {
get_interest, get_interest,
get_all_interests, get_all_interests,
get_all_local_interests, get_all_local_interests,
get_relay_list,
set_wallet, set_wallet,
load_wallet, load_wallet,
remove_wallet, remove_wallet,
@@ -173,6 +177,53 @@ fn main() {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
main_window.set_traffic_lights_inset(7.0, 10.0).unwrap(); 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 { let client = tauri::async_runtime::block_on(async move {
// Setup database // Setup database
let database = NostrLMDB::open(config_dir.join("nostr")) let database = NostrLMDB::open(config_dir.join("nostr"))
@@ -231,110 +282,9 @@ fn main() {
app.manage(Nostr { app.manage(Nostr {
client, client,
queue: RwLock::new(HashSet::new()), queue: RwLock::new(HashSet::new()),
is_syncing: RwLock::new(false),
settings: RwLock::new(Settings::default()), 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 // Listen for request metadata
app.listen_any("request_metadata", move |event| { app.listen_any("request_metadata", move |event| {
let payload = event.payload(); 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 { tauri::async_runtime::spawn(async move {
let state = handle_clone.state::<Nostr>(); let state = handle_clone.state::<Nostr>();
let client = &state.client; let client = &state.client;
@@ -499,8 +543,13 @@ fn main() {
.plugin(tauri_plugin_upload::init()) .plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_window_state::Builder::default().build())
.run(ctx) .build(ctx)
.expect("error while running tauri application"); .expect("error while running tauri application")
.run(|_app_handle, event| {
if let tauri::RunEvent::ExitRequested { api, .. } = event {
api.prevent_exit();
}
});
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
+9 -5
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Lume", "productName": "Lume",
"version": "24.11.1", "version": "24.11.4",
"identifier": "nu.lume.Lume", "identifier": "nu.lume.Lume",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
@@ -30,6 +30,13 @@
"$RESOURCE/*" "$RESOURCE/*"
] ]
} }
},
"trayIcon": {
"id": "main",
"iconAsTemplate": true,
"menuOnLeftClick": true,
"tooltip": "Lume",
"iconPath": "./icons/tray.png"
} }
}, },
"bundle": { "bundle": {
@@ -39,10 +46,7 @@
"targets": "all", "targets": "all",
"active": true, "active": true,
"category": "SocialNetworking", "category": "SocialNetworking",
"resources": [ "resources": ["resources/*", "locales/*"],
"resources/*",
"locales/*"
],
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
+1 -3
View File
@@ -13,9 +13,7 @@
"hiddenTitle": true, "hiddenTitle": true,
"transparent": true, "transparent": true,
"windowEffects": { "windowEffects": {
"effects": [ "effects": ["underWindowBackground"]
"underWindowBackground"
]
} }
} }
] ]
+8
View File
@@ -248,6 +248,14 @@ async getAllLocalInterests(until: string | null) : Promise<Result<RichEvent[], s
else return { status: "error", error: e as any }; 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>> { async setWallet(uri: string) : Promise<Result<boolean, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("set_wallet", { uri }) }; return { status: "ok", data: await TAURI_INVOKE("set_wallet", { uri }) };
+10 -38
View File
@@ -3,11 +3,9 @@ import type {
MaybePromise, MaybePromise,
PersistedQuery, PersistedQuery,
} from "@tanstack/query-persist-client-core"; } 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 { readFile } from "@tauri-apps/plugin-fs";
import { relaunch } from "@tauri-apps/plugin-process";
import type { Store as TauriStore } from "@tauri-apps/plugin-store"; import type { Store as TauriStore } from "@tauri-apps/plugin-store";
import { check } from "@tauri-apps/plugin-updater";
import { BitcoinUnit } from "bitcoin-units"; import { BitcoinUnit } from "bitcoin-units";
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from "clsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -23,6 +21,15 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); 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) => { export const isImagePath = (path: string) => {
const exts = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"]; 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) { export async function upload(filePath?: string) {
const allowExts = [ const allowExts = [
"png", "png",
+1 -1
View File
@@ -160,7 +160,7 @@ function Header({
<button <button
type="button" type="button"
onClick={(e) => showContextMenu(e)} 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" /> <CaretDown className="size-3" weight="bold" />
</button> </button>
+1 -1
View File
@@ -16,7 +16,7 @@ export function NoteQuote({
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<button <button
type="button" type="button"
onClick={() => LumeWindow.openEditor(null, event.id)} onClick={() => LumeWindow.openEditor(undefined, event.id)}
className={cn( className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200", "inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
label label
-28
View File
@@ -13,7 +13,6 @@ import { createFileRoute } from '@tanstack/react-router'
// Import Routes // Import Routes
import { Route as rootRoute } from './routes/__root' 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 AppImport } from './routes/_app'
import { Route as NewPostIndexImport } from './routes/new-post/index' import { Route as NewPostIndexImport } from './routes/new-post/index'
import { Route as AppIndexImport } from './routes/_app/index' import { Route as AppIndexImport } from './routes/_app/index'
@@ -91,14 +90,6 @@ const NewLazyRoute = NewLazyImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route)) } 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({ const AppRoute = AppImport.update({
id: '/_app', id: '/_app',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
@@ -389,13 +380,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppImport preLoaderRoute: typeof AppImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/bootstrap-relays': {
id: '/bootstrap-relays'
path: '/bootstrap-relays'
fullPath: '/bootstrap-relays'
preLoaderRoute: typeof BootstrapRelaysImport
parentRoute: typeof rootRoute
}
'/new': { '/new': {
id: '/new' id: '/new'
path: '/new' path: '/new'
@@ -758,7 +742,6 @@ const SettingsIdLazyRouteWithChildren = SettingsIdLazyRoute._addFileChildren(
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'': typeof AppRouteWithChildren '': typeof AppRouteWithChildren
'/bootstrap-relays': typeof BootstrapRelaysRoute
'/new': typeof NewLazyRoute '/new': typeof NewLazyRoute
'/$id/set-group': typeof IdSetGroupRoute '/$id/set-group': typeof IdSetGroupRoute
'/$id/set-interest': typeof IdSetInterestRoute '/$id/set-interest': typeof IdSetInterestRoute
@@ -797,7 +780,6 @@ export interface FileRoutesByFullPath {
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/bootstrap-relays': typeof BootstrapRelaysRoute
'/new': typeof NewLazyRoute '/new': typeof NewLazyRoute
'/$id/set-group': typeof IdSetGroupRoute '/$id/set-group': typeof IdSetGroupRoute
'/$id/set-interest': typeof IdSetInterestRoute '/$id/set-interest': typeof IdSetInterestRoute
@@ -838,7 +820,6 @@ export interface FileRoutesByTo {
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRoute __root__: typeof rootRoute
'/_app': typeof AppRouteWithChildren '/_app': typeof AppRouteWithChildren
'/bootstrap-relays': typeof BootstrapRelaysRoute
'/new': typeof NewLazyRoute '/new': typeof NewLazyRoute
'/$id/set-group': typeof IdSetGroupRoute '/$id/set-group': typeof IdSetGroupRoute
'/$id/set-interest': typeof IdSetInterestRoute '/$id/set-interest': typeof IdSetInterestRoute
@@ -881,7 +862,6 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths:
| '' | ''
| '/bootstrap-relays'
| '/new' | '/new'
| '/$id/set-group' | '/$id/set-group'
| '/$id/set-interest' | '/$id/set-interest'
@@ -919,7 +899,6 @@ export interface FileRouteTypes {
| '/columns/users/$id' | '/columns/users/$id'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/bootstrap-relays'
| '/new' | '/new'
| '/$id/set-group' | '/$id/set-group'
| '/$id/set-interest' | '/$id/set-interest'
@@ -958,7 +937,6 @@ export interface FileRouteTypes {
id: id:
| '__root__' | '__root__'
| '/_app' | '/_app'
| '/bootstrap-relays'
| '/new' | '/new'
| '/$id/set-group' | '/$id/set-group'
| '/$id/set-interest' | '/$id/set-interest'
@@ -1000,7 +978,6 @@ export interface FileRouteTypes {
export interface RootRouteChildren { export interface RootRouteChildren {
AppRoute: typeof AppRouteWithChildren AppRoute: typeof AppRouteWithChildren
BootstrapRelaysRoute: typeof BootstrapRelaysRoute
NewLazyRoute: typeof NewLazyRoute NewLazyRoute: typeof NewLazyRoute
IdSetGroupRoute: typeof IdSetGroupRoute IdSetGroupRoute: typeof IdSetGroupRoute
IdSetInterestRoute: typeof IdSetInterestRoute IdSetInterestRoute: typeof IdSetInterestRoute
@@ -1016,7 +993,6 @@ export interface RootRouteChildren {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
AppRoute: AppRouteWithChildren, AppRoute: AppRouteWithChildren,
BootstrapRelaysRoute: BootstrapRelaysRoute,
NewLazyRoute: NewLazyRoute, NewLazyRoute: NewLazyRoute,
IdSetGroupRoute: IdSetGroupRoute, IdSetGroupRoute: IdSetGroupRoute,
IdSetInterestRoute: IdSetInterestRoute, IdSetInterestRoute: IdSetInterestRoute,
@@ -1043,7 +1019,6 @@ export const routeTree = rootRoute
"filePath": "__root.tsx", "filePath": "__root.tsx",
"children": [ "children": [
"/_app", "/_app",
"/bootstrap-relays",
"/new", "/new",
"/$id/set-group", "/$id/set-group",
"/$id/set-interest", "/$id/set-interest",
@@ -1063,9 +1038,6 @@ export const routeTree = rootRoute
"/_app/" "/_app/"
] ]
}, },
"/bootstrap-relays": {
"filePath": "bootstrap-relays.tsx"
},
"/new": { "/new": {
"filePath": "new.lazy.tsx" "filePath": "new.lazy.tsx"
}, },
+38 -11
View File
@@ -1,14 +1,41 @@
import { commands } from '@/commands.gen' import { commands } from "@/commands.gen";
import { createFileRoute, redirect } from '@tanstack/react-router' 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')({ async function checkForAppUpdates() {
beforeLoad: async () => { const update = await check();
const accounts = await commands.getAccounts()
if (!accounts.length) { if (update?.available) {
throw redirect({ to: '/new', replace: true }) 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 };
},
});
-159
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>
);
}
-14
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);
}
},
});
@@ -93,7 +93,7 @@ function Screen() {
data?.map((item) => ( data?.map((item) => (
<div <div
key={item} 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.Provider pubkey={item}>
<User.Root> <User.Root>
@@ -143,12 +143,14 @@ function Screen() {
</p> </p>
</div> </div>
) : isError ? ( ) : 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"> <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-sm text-center">{error?.message ?? "Error"}</p> <p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
{error?.message ?? "Error"}
</p>
</div> </div>
) : !data?.length ? ( ) : !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"> <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-sm text-center"> <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 Nothing to show yet, you can use Lume more and comeback lack to
see new events. see new events.
</p> </p>
@@ -156,6 +158,13 @@ function Screen() {
) : ( ) : (
data?.map((item) => renderItem(item)) 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 ? ( {hasNextPage ? (
<button <button
type="button" type="button"
@@ -137,12 +137,14 @@ function Screen() {
</p> </p>
</div> </div>
) : isError ? ( ) : 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"> <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-sm text-center">{error?.message ?? "Error"}</p> <p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
{error?.message ?? "Error"}
</p>
</div> </div>
) : !data?.length ? ( ) : !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"> <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-sm text-center"> <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 Nothing to show yet, you can use Lume more and comeback lack to
see new events. see new events.
</p> </p>
@@ -150,6 +152,13 @@ function Screen() {
) : ( ) : (
data?.map((item) => renderItem(item)) 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 ? ( {hasNextPage ? (
<button <button
type="button" type="button"
@@ -115,12 +115,14 @@ function Screen() {
</p> </p>
</div> </div>
) : isError ? ( ) : 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"> <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-sm text-center">{error?.message ?? "Error"}</p> <p className="text-xs text-center px-4 text-neutral-500 dark:text-neutral-400">
{error?.message ?? "Error"}
</p>
</div> </div>
) : !data?.length ? ( ) : !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"> <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-sm text-center"> <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 Nothing to show yet, you can use Lume more and comeback lack to
see new events. see new events.
</p> </p>
@@ -128,6 +130,13 @@ function Screen() {
) : ( ) : (
data?.map((item) => renderItem(item)) 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 ? ( {hasNextPage ? (
<button <button
type="button" type="button"
@@ -175,7 +175,6 @@ function ReplyList() {
return events.filter((ev) => !removeQueues.has(ev.id)); return events.filter((ev) => !removeQueues.has(ev.id));
}, },
refetchOnWindowFocus: false,
}); });
useEffect(() => { useEffect(() => {
@@ -88,7 +88,7 @@ export function Screen() {
> >
<ScrollArea.Viewport <ScrollArea.Viewport
ref={ref} 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>}> <Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
{isFetching && !isLoading && !isFetchingNextPage ? ( {isFetching && !isLoading && !isFetchingNextPage ? (
+181 -22
View File
@@ -1,5 +1,5 @@
import { commands } from "@/commands.gen"; import { commands } from "@/commands.gen";
import { cn, toLumeEvents } from "@/commons"; import { cn, isValidRelayUrl, toLumeEvents } from "@/commons";
import { Spinner, User } from "@/components"; import { Spinner, User } from "@/components";
import { LumeWindow } from "@/system"; import { LumeWindow } from "@/system";
import type { LumeColumn, NostrEvent } from "@/types"; 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 { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path"; import { resolveResource } from "@tauri-apps/api/path";
import { message } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs"; import { readTextFile } from "@tauri-apps/plugin-fs";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { useCallback } from "react"; import { useCallback, useState, useTransition } from "react";
export const Route = createLazyFileRoute("/columns/_layout/launchpad/$id")({ export const Route = createLazyFileRoute("/columns/_layout/launchpad/$id")({
component: Screen, component: Screen,
@@ -25,6 +26,7 @@ function Screen() {
> >
<ScrollArea.Viewport className="relative h-full px-3 pb-3"> <ScrollArea.Viewport className="relative h-full px-3 pb-3">
<Newsfeeds /> <Newsfeeds />
<Relayfeeds />
<Interests /> <Interests />
<Core /> <Core />
</ScrollArea.Viewport> </ScrollArea.Viewport>
@@ -178,14 +180,14 @@ function Newsfeeds() {
type="button" type="button"
onClick={() => onClick={() =>
LumeWindow.openColumn({ LumeWindow.openColumn({
name: "Newsfeeds", name: "Browse Newsfeeds",
url: "/columns/discover-newsfeeds", url: "/columns/discover-newsfeeds",
label: "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" 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" /> <ArrowRight className="size-4" weight="bold" />
</button> </button>
</div> </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() { function Interests() {
const { id } = Route.useParams(); const { id } = Route.useParams();
const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({ const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({
@@ -332,14 +507,14 @@ function Interests() {
type="button" type="button"
onClick={() => onClick={() =>
LumeWindow.openColumn({ LumeWindow.openColumn({
name: "Interests", name: "Browse Interests",
url: "/columns/discover-interests", url: "/columns/discover-interests",
label: "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" 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" /> <ArrowRight className="size-4" weight="bold" />
</button> </button>
</div> </div>
@@ -391,22 +566,6 @@ function Core() {
Add Add
</button> </button>
</div> </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) => ( {data?.map((column) => (
<div <div
key={column.label} key={column.label}
+6 -6
View File
@@ -27,32 +27,32 @@ function Screen() {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<a <a
href="/new-account/connect" 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> <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 Your account will be handled by a remote signer. Lume will not
store your account keys. store your account keys.
</p> </p>
</a> </a>
<a <a
href="/new-account/import" 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> <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 Lume will store your keys in secure storage. You can provide a
password to add extra security. password to add extra security.
</p> </p>
</a> </a>
<a <a
href="/new-account/watch" 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"> <h3 className="mb-1 font-medium">
Continue with Public Key (Watch Mode) Continue with Public Key (Watch Mode)
</h3> </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 Use for experience without provide your private key, you can add
it later to publish new note. it later to publish new note.
</p> </p>
+1 -4
View File
@@ -1,10 +1,7 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: ["./src/**/*.{js,ts,jsx,tsx}", "index.html"],
"./src/**/*.{js,ts,jsx,tsx}",
"index.html",
],
theme: { theme: {
extend: { extend: {
keyframes: { keyframes: {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

+4 -13
View File
@@ -2,19 +2,12 @@
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": [ "lib": ["ESNext", "ES2020", "DOM", "DOM.Iterable"],
"ESNext",
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"@/*": [ "@/*": ["./src/*"]
"./src/*"
]
}, },
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@@ -26,11 +19,9 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitAny": false, "noImplicitAny": false,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true
}, },
"include": [ "include": ["src"],
"src"
],
"references": [ "references": [
{ {
"path": "./tsconfig.node.json" "path": "./tsconfig.node.json"
+1 -3
View File
@@ -6,7 +6,5 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"include": [ "include": ["vite.config.ts"]
"vite.config.ts"
]
} }