feat: negentropy progress

This commit is contained in:
2024-10-25 14:57:12 +07:00
parent 055d73c829
commit 5ab2b1ae31
27 changed files with 769 additions and 663 deletions

View File

@@ -15,6 +15,7 @@
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",

26
pnpm-lock.yaml generated
View File

@@ -23,6 +23,9 @@ importers:
'@radix-ui/react-popover':
specifier: ^1.1.2
version: 1.1.2(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
'@radix-ui/react-progress':
specifier: ^1.1.0
version: 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
'@radix-ui/react-scroll-area':
specifier: ^1.2.0
version: 1.2.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
@@ -974,6 +977,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-progress@1.1.0':
resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.0':
resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==}
peerDependencies:
@@ -3044,6 +3060,16 @@ snapshots:
'@types/react': types-react@19.0.0-rc.1
'@types/react-dom': types-react-dom@19.0.0-rc.1
'@radix-ui/react-progress@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)':
dependencies:
'@radix-ui/react-context': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)
'@radix-ui/react-primitive': 2.0.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
react: 19.0.0-rc-d025ddd3-20240722
react-dom: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722)
optionalDependencies:
'@types/react': types-react@19.0.0-rc.1
'@types/react-dom': types-react-dom@19.0.0-rc.1
'@radix-ui/react-roving-focus@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)':
dependencies:
'@radix-ui/primitive': 1.1.0

84
src-tauri/Cargo.lock generated
View File

@@ -44,6 +44,7 @@ dependencies = [
"tauri-plugin-window-state",
"tauri-specta",
"tokio",
"tracing-subscriber",
"url",
]
@@ -3479,7 +3480,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "nostr"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33"
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
dependencies = [
"aes",
"base64 0.22.1",
@@ -3509,7 +3510,7 @@ dependencies = [
[[package]]
name = "nostr-database"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33"
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
dependencies = [
"async-trait",
"flatbuffers",
@@ -3523,7 +3524,7 @@ dependencies = [
[[package]]
name = "nostr-lmdb"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33"
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
dependencies = [
"heed",
"nostr",
@@ -3536,7 +3537,7 @@ dependencies = [
[[package]]
name = "nostr-relay-pool"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33"
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
dependencies = [
"async-utility",
"async-wsocket",
@@ -3554,7 +3555,7 @@ dependencies = [
[[package]]
name = "nostr-sdk"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33"
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
dependencies = [
"async-utility",
"atomic-destructor",
@@ -3574,7 +3575,7 @@ dependencies = [
[[package]]
name = "nostr-signer"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33"
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
dependencies = [
"async-utility",
"nostr",
@@ -3587,7 +3588,7 @@ dependencies = [
[[package]]
name = "nostr-zapper"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33"
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
dependencies = [
"async-trait",
"nostr",
@@ -3607,6 +3608,16 @@ dependencies = [
"zbus 4.4.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num"
version = "0.4.3"
@@ -3721,7 +3732,7 @@ dependencies = [
[[package]]
name = "nwc"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33"
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
dependencies = [
"async-utility",
"nostr",
@@ -3993,6 +4004,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "page_size"
version = "0.6.0"
@@ -5316,6 +5333,15 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "share-picker"
version = "0.1.0"
@@ -6331,6 +6357,16 @@ dependencies = [
"syn 2.0.79",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "tiff"
version = "0.9.1"
@@ -6606,6 +6642,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -6813,6 +6875,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"

View File

@@ -48,6 +48,7 @@ linkify = "0.10.0"
regex = "1.10.4"
keyring = { version = "3", features = ["apple-native", "windows-native"] }
keyring-search = "1.2.0"
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
[target.'cfg(target_os = "macos")'.dependencies]
border = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }

View File

@@ -1,4 +1,4 @@
wss://relay.damus.io,
wss://relay.nostr.net,
wss://relay.primal.net,
wss://nostr.fmt.wiz.biz,
wss://offchain.pub,

View File

@@ -1,15 +1,12 @@
use async_utility::thread::sleep;
use keyring::Entry;
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::{fs, str::FromStr, time::Duration};
use tauri::{Emitter, Manager, State};
use std::{str::FromStr, time::Duration};
use tauri::{Emitter, State};
use crate::{common::get_all_accounts, Nostr};
use super::sync::sync_account;
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
struct Account {
secret_key: String,
@@ -24,11 +21,8 @@ pub fn get_accounts() -> Vec<String> {
#[tauri::command]
#[specta::specta]
pub async fn watch_account(
id: String,
state: State<'_, Nostr>,
app_handle: tauri::AppHandle,
) -> Result<String, String> {
pub async fn watch_account(id: String, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
let npub = public_key.to_bech32().map_err(|e| e.to_string())?;
let keyring = Entry::new("Lume Safe Storage", &npub).map_err(|e| e.to_string())?;
@@ -36,14 +30,14 @@ pub async fn watch_account(
// Set empty password
keyring.set_password("").map_err(|e| e.to_string())?;
// Run sync for this account
sync_account(public_key, app_handle);
// Update state
state.accounts.lock().unwrap().push(npub.clone());
let mut accounts = state.accounts.lock().unwrap().clone();
accounts.push(npub.clone());
// Fake loading
sleep(Duration::from_secs(4)).await;
// Get user's profile
let _ = client
.fetch_metadata(public_key, Some(Duration::from_secs(4)))
.await;
Ok(npub)
}
@@ -54,7 +48,6 @@ pub async fn import_account(
key: String,
password: Option<String>,
state: State<'_, Nostr>,
app_handle: tauri::AppHandle,
) -> Result<String, String> {
let client = &state.client;
@@ -87,25 +80,21 @@ pub async fn import_account(
// Update signer
client.set_signer(Some(signer)).await;
// Run sync for this account
sync_account(public_key, app_handle);
// Fake loading
sleep(Duration::from_secs(4)).await;
// Update state
state.accounts.lock().unwrap().push(npub.clone());
let mut accounts = state.accounts.lock().unwrap().clone();
accounts.push(npub.clone());
// Get user's profile
let _ = client
.fetch_metadata(public_key, Some(Duration::from_secs(4)))
.await;
Ok(npub)
}
#[tauri::command]
#[specta::specta]
pub async fn connect_account(
uri: String,
state: State<'_, Nostr>,
app_handle: tauri::AppHandle,
) -> Result<String, String> {
pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
match NostrConnectURI::parse(uri.clone()) {
@@ -118,9 +107,6 @@ pub async fn connect_account(
let remote_user = bunker_uri.signer_public_key().unwrap();
let remote_npub = remote_user.to_bech32().unwrap();
// Run sync for this account
sync_account(remote_user, app_handle);
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None) {
Ok(signer) => {
let mut url = Url::parse(&uri).unwrap();
@@ -146,7 +132,13 @@ pub async fn connect_account(
let _ = client.set_signer(Some(signer.into())).await;
// Update state
state.accounts.lock().unwrap().push(remote_npub.clone());
let mut accounts = state.accounts.lock().unwrap().clone();
accounts.push(remote_npub.clone());
// Get user's profile
let _ = client
.fetch_metadata(remote_user, Some(Duration::from_secs(4)))
.await;
Ok(remote_npub)
}
@@ -198,24 +190,6 @@ pub fn delete_account(id: String) -> Result<(), String> {
Ok(())
}
#[tauri::command]
#[specta::specta]
pub async fn is_new_account(id: String, app_handle: tauri::AppHandle) -> Result<bool, String> {
let config_dir = app_handle.path().config_dir().map_err(|e| e.to_string())?;
let exist = fs::metadata(config_dir.join(id)).is_ok();
Ok(!exist)
}
#[tauri::command]
#[specta::specta]
pub async fn toggle_new_account(id: String, app_handle: tauri::AppHandle) -> Result<(), String> {
let config_dir = app_handle.path().config_dir().map_err(|e| e.to_string())?;
fs::File::create(config_dir.join(id)).unwrap();
Ok(())
}
#[tauri::command]
#[specta::specta]
pub async fn has_signer(id: String, state: State<'_, Nostr>) -> Result<bool, String> {

View File

@@ -1,11 +1,10 @@
use futures::future::join_all;
use nostr_sdk::prelude::*;
use serde::Serialize;
use specta::Type;
use std::{str::FromStr, time::Duration};
use tauri::State;
use crate::common::{create_tags, get_latest_event, parse_event, process_event, Meta};
use crate::common::{create_tags, parse_event, process_event, Meta};
use crate::{Nostr, DEFAULT_DIFFICULTY, FETCH_LIMIT};
#[derive(Debug, Clone, Serialize, Type)]
@@ -14,23 +13,15 @@ pub struct RichEvent {
pub parsed: Option<Meta>,
}
#[tauri::command]
#[specta::specta]
pub async fn get_event_meta(content: String) -> Result<Meta, ()> {
let meta = parse_event(&content).await;
Ok(meta)
}
#[tauri::command]
#[specta::specta]
pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent, String> {
let client = &state.client;
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
let filter = Filter::new().id(event_id);
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
match client.database().query(vec![filter.clone()]).await {
match client.database().event_by_id(&event_id).await {
Ok(events) => {
if let Some(event) = get_latest_event(&events) {
if let Some(event) = events {
let raw = event.as_json();
let parsed = if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
@@ -40,26 +31,7 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
Ok(RichEvent { raw, parsed })
} else {
match client
.fetch_events(vec![filter], Some(Duration::from_secs(10)))
.await
{
Ok(events) => {
if let Some(event) = get_latest_event(&events) {
let raw = event.as_json();
let parsed = if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
} else {
None
};
Ok(RichEvent { raw, parsed })
} else {
Err("Not found.".into())
}
}
Err(err) => Err(err.to_string()),
}
Err("Event not found".to_string())
}
}
Err(err) => Err(err.to_string()),
@@ -68,35 +40,8 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
#[tauri::command]
#[specta::specta]
pub async fn get_event_from(
id: String,
_relay_hint: String,
state: State<'_, Nostr>,
) -> Result<RichEvent, String> {
let client = &state.client;
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
let filter = Filter::new().id(event_id);
match client
.fetch_events(vec![filter], Some(Duration::from_secs(5)))
.await
{
Ok(events) => {
if let Some(event) = events.first() {
let raw = event.as_json();
let parsed = if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
} else {
None
};
Ok(RichEvent { raw, parsed })
} else {
Err("Cannot found this event with current relay list".into())
}
}
Err(err) => Err(err.to_string()),
}
pub async fn get_meta_from_event(content: String) -> Result<Meta, ()> {
Ok(parse_event(&content).await)
}
#[tauri::command]
@@ -110,21 +55,7 @@ pub async fn get_replies(id: String, state: State<'_, Nostr>) -> Result<Vec<Rich
.fetch_events(vec![filter], Some(Duration::from_secs(5)))
.await
{
Ok(events) => {
let futures = events.iter().map(|ev| async move {
let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote {
Some(parse_event(&ev.content).await)
} else {
None
};
RichEvent { raw, parsed }
});
let rich_events = join_all(futures).await;
Ok(rich_events)
}
Ok(events) => Ok(process_event(client, events).await),
Err(err) => Err(err.to_string()),
}
}

View File

@@ -237,7 +237,7 @@ pub async fn set_group(
.authors(public_keys)
.limit(500);
if let Ok(report) = client.sync(filter, NegentropyOptions::default()).await {
if let Ok(report) = client.sync(filter, SyncOptions::default()).await {
println!("Received: {}", report.received.len());
handle.emit("synchronized", ()).unwrap();
};
@@ -331,7 +331,7 @@ pub async fn set_interest(
.hashtags(hashtags)
.limit(500);
if let Ok(report) = client.sync(filter, NegentropyOptions::default()).await {
if let Ok(report) = client.sync(filter, SyncOptions::default()).await {
println!("Received: {}", report.received.len());
handle.emit("synchronized", ()).unwrap();
};
@@ -560,15 +560,12 @@ pub async fn get_notifications(id: String, state: State<'_, Nostr>) -> Result<Ve
let client = &state.client;
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(200);
let filter = Filter::new().pubkey(public_key).kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
]);
match client.database().query(vec![filter]).await {
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),

View File

@@ -1,8 +1,11 @@
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::str::FromStr;
use tauri::{AppHandle, Manager};
use std::{
fs::{self, File},
str::FromStr,
};
use tauri::{ipc::Channel, AppHandle, Manager, State};
use tauri_specta::Event as TauriEvent;
use crate::Nostr;
@@ -45,70 +48,19 @@ pub fn sync_all(accounts: Vec<String>, app_handle: AppHandle) {
let client = &state.client;
let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone();
// NEG: Sync metadata
//
let metadata = Filter::new().authors(public_keys.clone()).kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::EventDeletion,
Kind::TextNote,
Kind::Repost,
Kind::Custom(30315),
]);
if let Ok(report) = client
.sync_with(&bootstrap_relays, metadata, NegentropyOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Others,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
// NEG: Sync notification
//
let notification = Filter::new()
.pubkeys(public_keys)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(5000);
if let Ok(report) = client
.sync_with(
&bootstrap_relays,
notification,
NegentropyOptions::default(),
)
.await
{
NegentropyEvent {
kind: NegentropyKind::Notification,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
// NEG: Sync events for all pubkeys in local database
//
let pubkey_filter = Filter::new().kinds(vec![
Kind::ContactList,
Kind::Repost,
Kind::TextNote,
Kind::FollowSet,
]);
if let Ok(events) = client.database().query(vec![pubkey_filter]).await {
if let Ok(events) = client
.database()
.query(vec![Filter::new().kinds(vec![
Kind::ContactList,
Kind::FollowSet,
Kind::MuteList,
Kind::Repost,
Kind::TextNote,
])])
.await
{
let pubkeys: Vec<PublicKey> = events
.iter()
.flat_map(|ev| ev.tags.public_keys().copied())
@@ -126,15 +78,15 @@ pub fn sync_all(accounts: Vec<String>, app_handle: AppHandle) {
let events = Filter::new()
.authors(authors.clone())
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(5000);
.limit(1000);
if let Ok(report) = client
.sync_with(&bootstrap_relays, events, NegentropyOptions::default())
if let Ok(output) = client
.sync_with(&bootstrap_relays, events, SyncOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Events,
total_event: report.received.len() as i32,
total_event: output.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
@@ -144,124 +96,35 @@ pub fn sync_all(accounts: Vec<String>, app_handle: AppHandle) {
//
let metadata = Filter::new()
.authors(authors)
.kinds(vec![Kind::Metadata, Kind::ContactList]);
.kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::MuteList,
Kind::RelaySet,
])
.limit(1000);
if let Ok(report) = client
.sync_with(&bootstrap_relays, metadata, NegentropyOptions::default())
if let Ok(output) = client
.sync_with(&bootstrap_relays, metadata, SyncOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Metadata,
total_event: report.received.len() as i32,
total_event: output.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
}
}
});
}
pub fn sync_account(public_key: PublicKey, app_handle: AppHandle) {
tauri::async_runtime::spawn(async move {
let state = app_handle.state::<Nostr>();
let client = &state.client;
let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone();
// NEG: Sync all user's metadata
//
let metadata = Filter::new().author(public_key).kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::RelayList,
Kind::RelaySet,
Kind::EventDeletion,
Kind::Custom(30315),
]);
if let Ok(report) = client
.sync_with(&bootstrap_relays, metadata, NegentropyOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Metadata,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
if let Ok(contact_list) = client.database().contacts_public_keys(public_key).await {
// NEG: Sync all contact's metadata
//
let metadata = Filter::new()
.authors(contact_list.clone())
.kinds(vec![Kind::Metadata, Kind::RelaySet, Kind::Custom(30315)])
.limit(1000);
if let Ok(report) = client
.sync_with(&bootstrap_relays, metadata, NegentropyOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Metadata,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
// NEG: Sync all contact's events
//
let metadata = Filter::new()
.authors(contact_list.clone())
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(1000);
if let Ok(report) = client
.sync_with(&bootstrap_relays, metadata, NegentropyOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Events,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
// NEG: Sync all contact's other metadata
//
let metadata = Filter::new()
.authors(contact_list)
.kinds(vec![
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::EventDeletion,
])
.limit(1000);
if let Ok(report) = client
.sync_with(&bootstrap_relays, metadata, NegentropyOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Metadata,
total_event: report.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
}
// NEG: Sync all user's metadata
// NEG: Sync notification
//
let notification = Filter::new()
.pubkey(public_key)
.pubkeys(public_keys.clone())
.kinds(vec![
Kind::TextNote,
Kind::Repost,
@@ -270,20 +133,188 @@ pub fn sync_account(public_key: PublicKey, app_handle: AppHandle) {
])
.limit(500);
if let Ok(report) = client
.sync_with(
&bootstrap_relays,
notification,
NegentropyOptions::default(),
)
if let Ok(output) = client
.sync_with(&bootstrap_relays, notification, SyncOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Notification,
total_event: report.received.len() as i32,
total_event: output.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
// NEG: Sync metadata
//
let metadata = Filter::new().authors(public_keys.clone()).kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::RelayList,
Kind::MuteList,
Kind::EventDeletion,
Kind::Bookmarks,
Kind::BookmarkSet,
Kind::Emojis,
Kind::EmojiSet,
Kind::TextNote,
Kind::Repost,
Kind::Custom(30315),
]);
if let Ok(output) = client
.sync_with(&bootstrap_relays, metadata, SyncOptions::default())
.await
{
NegentropyEvent {
kind: NegentropyKind::Others,
total_event: output.received.len() as i32,
}
.emit(&app_handle)
.unwrap();
}
});
}
#[tauri::command]
#[specta::specta]
pub fn is_account_sync(id: String, app_handle: tauri::AppHandle) -> Result<bool, String> {
let config_dir = app_handle
.path()
.app_config_dir()
.map_err(|e| e.to_string())?;
let exist = fs::metadata(config_dir.join(id)).is_ok();
Ok(exist)
}
#[tauri::command]
#[specta::specta]
pub async fn sync_account(
id: String,
state: State<'_, Nostr>,
reader: Channel<f64>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
let client = &state.client;
let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone();
let public_key = PublicKey::from_bech32(&id).map_err(|e| e.to_string())?;
let filter = Filter::new().author(public_key).kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::RelayList,
Kind::MuteList,
Kind::EventDeletion,
Kind::Bookmarks,
Kind::BookmarkSet,
Kind::TextNote,
Kind::Repost,
Kind::Custom(30315),
]);
let (tx, mut rx) = SyncProgress::channel();
let opts = SyncOptions::default().progress(tx);
tauri::async_runtime::spawn(async move {
while (rx.changed().await).is_ok() {
let SyncProgress { total, current } = *rx.borrow_and_update();
if total > 0 {
reader
.send((current as f64 / total as f64) * 100.0)
.unwrap()
}
}
});
if let Ok(output) = client
.sync_with(&bootstrap_relays, filter, opts.clone())
.await
{
println!("Success: {:?}", output.success);
println!("Failed: {:?}", output.failed);
let event_pubkeys = client
.database()
.query(vec![Filter::new().kinds(vec![
Kind::ContactList,
Kind::FollowSet,
Kind::MuteList,
Kind::Repost,
Kind::TextNote,
])])
.await
.map_err(|e| e.to_string())?;
if !event_pubkeys.is_empty() {
let pubkeys: Vec<PublicKey> = event_pubkeys
.iter()
.flat_map(|ev| ev.tags.public_keys().copied())
.collect();
let filter = Filter::new()
.authors(pubkeys)
.kinds(vec![
Kind::Metadata,
Kind::TextNote,
Kind::Repost,
Kind::EventDeletion,
Kind::Interests,
Kind::InterestSet,
Kind::FollowSet,
Kind::RelayList,
Kind::MuteList,
Kind::EventDeletion,
Kind::Bookmarks,
Kind::BookmarkSet,
Kind::Custom(30315),
])
.limit(10000);
if let Ok(output) = client
.sync_with(&bootstrap_relays, filter, opts.clone())
.await
{
println!("Success: {:?}", output.success);
println!("Failed: {:?}", output.failed);
}
};
}
let event_ids = client
.database()
.query(vec![Filter::new().kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Bookmarks,
Kind::BookmarkSet,
])])
.await
.map_err(|e| e.to_string())?;
if !event_ids.is_empty() {
let ids: Vec<EventId> = event_ids.iter().map(|ev| ev.id).collect();
let filter = Filter::new().events(ids);
if let Ok(output) = client.sync_with(&bootstrap_relays, filter, opts).await {
println!("Success: {:?}", output.success);
println!("Failed: {:?}", output.failed);
}
}
let config_dir = app_handle
.path()
.app_config_dir()
.map_err(|e| e.to_string())?;
let _ = File::create(config_dir.join(id));
Ok(())
}

View File

@@ -34,14 +34,12 @@ const NOSTR_EVENTS: [&str; 10] = [
"Nostr:nevent1",
];
const NOSTR_MENTIONS: [&str; 10] = [
const NOSTR_MENTIONS: [&str; 8] = [
"@npub1",
"nostr:npub1",
"nostr:nprofile1",
"nostr:naddr1",
"npub1",
"nprofile1",
"naddr1",
"Nostr:npub1",
"Nostr:nprofile1",
"Nostr:naddr1",

View File

@@ -5,14 +5,7 @@
#[cfg(target_os = "macos")]
use border::WebviewWindowExt as BorderWebviewWindowExt;
use commands::{
account::*,
event::*,
metadata::*,
relay::*,
sync::{sync_all, NegentropyEvent},
window::*,
};
use commands::{account::*, event::*, metadata::*, relay::*, sync::*, window::*};
use common::{get_all_accounts, parse_event};
use nostr_sdk::prelude::{Profile as DatabaseProfile, *};
use serde::{Deserialize, Serialize};
@@ -97,8 +90,12 @@ pub const FETCH_LIMIT: usize = 50;
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
fn main() {
tracing_subscriber::fmt::init();
let builder = Builder::<tauri::Wry>::new()
.commands(collect_commands![
sync_account,
is_account_sync,
get_relays,
connect_relay,
remove_relay,
@@ -111,8 +108,6 @@ fn main() {
get_private_key,
delete_account,
reset_password,
is_new_account,
toggle_new_account,
has_signer,
set_signer,
get_profile,
@@ -138,9 +133,8 @@ fn main() {
get_user_settings,
set_user_settings,
verify_nip05,
get_event_meta,
get_meta_from_event,
get_event,
get_event_from,
get_replies,
subscribe_to,
get_all_events_by_author,

View File

@@ -5,6 +5,22 @@
export const commands = {
async syncAccount(id: string, reader: TAURI_CHANNEL<number>) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("sync_account", { id, reader }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async isAccountSync(id: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("is_account_sync", { id }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getRelays(id: string) : Promise<Result<Relays, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_relays", { id }) };
@@ -96,22 +112,6 @@ async resetPassword(key: string, password: string) : Promise<Result<null, string
else return { status: "error", error: e as any };
}
},
async isNewAccount(id: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("is_new_account", { id }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async toggleNewAccount(id: string) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("toggle_new_account", { id }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async hasSigner(id: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("has_signer", { id }) };
@@ -312,9 +312,9 @@ async verifyNip05(id: string, nip05: string) : Promise<Result<boolean, string>>
else return { status: "error", error: e as any };
}
},
async getEventMeta(content: string) : Promise<Result<Meta, null>> {
async getMetaFromEvent(content: string) : Promise<Result<Meta, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_event_meta", { content }) };
return { status: "ok", data: await TAURI_INVOKE("get_meta_from_event", { content }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -328,14 +328,6 @@ async getEvent(id: string) : Promise<Result<RichEvent, string>> {
else return { status: "error", error: e as any };
}
},
async getEventFrom(id: string, relayHint: string) : Promise<Result<RichEvent, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_event_from", { id, relayHint }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getReplies(id: string) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_replies", { id }) };

View File

@@ -1,7 +1,7 @@
import { appSettings, cn } from "@/commons";
import { useStore } from "@tanstack/react-store";
import { nanoid } from "nanoid";
import { type ReactNode, memo, useMemo, useState } from "react";
import { type ReactNode, useMemo, useState } from "react";
import reactStringReplace from "react-string-replace";
import { Hashtag } from "./mentions/hashtag";
import { MentionNote } from "./mentions/note";
@@ -22,7 +22,6 @@ export function NoteContent({
}) {
const event = useNoteContext();
const visible = useStore(appSettings, (state) => state.display_media);
const warning = useMemo(() => event.warning, [event]);
const content = useMemo(() => {
try {
// Get parsed meta
@@ -88,52 +87,48 @@ export function NoteContent({
}
}, [event.content]);
const [blurred, setBlurred] = useState(() =>
event.warning ? event.warning.length > 0 : false,
);
return (
<div className="relative flex flex-col gap-2">
<ContentWarning warning={warning} />
<div
className={cn(
"select-text text-pretty content-break overflow-hidden",
className,
)}
>
{content}
</div>
{visible ? (
event.meta?.images.length ? (
<Images urls={event.meta.images} />
) : null
) : null}
{!blurred ? (
<>
<div
className={cn(
"select-text text-pretty content-break overflow-hidden",
className,
)}
>
{content}
</div>
{visible ? (
event.meta?.images.length ? (
<Images urls={event.meta.images} />
) : null
) : null}
</>
) : (
<div
className={cn(
"select-text text-pretty content-break overflow-hidden",
className,
)}
>
<p className="text-yellow-600 dark:text-yellow-400">
The content is hidden because the author marked it with a warning
for a reason: <span className="font-semibold">{event.warning}</span>
</p>
<button
type="button"
onClick={() => setBlurred(false)}
className="font-medium text-sm text-blue-500 hover:text-blue-600"
>
View anyway
</button>
</div>
)}
</div>
);
}
const ContentWarning = memo(function ContentWarning({
warning,
}: { warning: string }) {
const [blurred, setBlurred] = useState(() => warning?.length > 0);
if (!blurred) {
return null;
}
return (
<div className="absolute inset-0 z-10 flex items-center justify-center w-full bg-black/80 backdrop-blur-lg">
<div className="flex flex-col items-center justify-center gap-2 text-center">
<p className="text-sm text-white/60">
The content is hidden because the author
<br />
marked it with a warning for a reason:
</p>
<p className="text-sm font-medium text-white">{warning}</p>
<button
type="button"
onClick={() => setBlurred(false)}
className="inline-flex items-center justify-center px-2 mt-4 text-sm font-medium border rounded-lg text-white/70 h-9 w-max bg-white/20 hover:bg-white/30 border-white/5"
>
View anyway
</button>
</div>
</div>
);
});

View File

@@ -17,16 +17,19 @@ export const MentionNote = memo(function MentionNote({
return (
<div className="relative my-2">
<div className="min-h-[64px] pl-3 before:content-[''] before:absolute before:top-1.5 before:bottom-1.5 before:left-0 before:border-l-[2px] before:border-black/10 dark:before:border-white/10">
<div className="pl-3 before:content-[''] before:absolute before:top-1.5 before:bottom-1.5 before:left-0 before:border-l-[2px] before:border-black/10 dark:before:border-white/10">
{isLoading ? (
<div className="h-[64px] flex items-center">
<div className="h-[32px] flex items-center gap-2 text-sm">
<Spinner />
Loadng note
</div>
) : isError || !event ? (
<div className="h-[64px] flex items-center">
<div className="flex flex-col break-all">
<p className="text-sm font-medium text-red-500">
{error.message || "Note can be found with your current relay set"}
{error?.message ??
"Cannot found this note within your current relay set"}
</p>
<p className="text-sm">{eventId}</p>
</div>
) : (
<Note.Provider event={event}>

View File

@@ -39,10 +39,6 @@ export function ImagePreview({ url }: { url: string }) {
decoding="async"
style={{ contentVisibility: "auto" }}
className="max-h-[400px] w-full h-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</div>
);

View File

@@ -21,9 +21,15 @@ export function Images({ urls }: { urls: string[] }) {
let newUrls: string[];
if (urls.length === 1) {
newUrls = urls.map(
(url) => `${service}?url=${url}&ll&af&default=1&n=-1`,
);
newUrls = urls.map((url) => {
if (url.includes("_next/")) {
return url;
}
if (url.includes("bsky.network")) {
return url;
}
return `${service}?url=${url}&ll&af&default=1&n=-1`;
});
} else {
newUrls = urls.map(
(url) => `${service}?url=${url}&w=480&h=640&ll&af&default=1&n=-1`,
@@ -83,10 +89,6 @@ export function Images({ urls }: { urls: string[] }) {
className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => urls[0]}
onKeyDown={() => urls[0]}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</div>
);
@@ -162,10 +164,6 @@ function LazyImage({ url, inView }: { url: string; inView: boolean }) {
onClick={() => open(url)}
onKeyDown={() => open(url)}
onLoad={setLoaded}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</div>
);

View File

@@ -18,6 +18,9 @@ export function UserAvatar({ className }: { className?: string }) {
if (user.profile?.picture.includes("_next/")) {
return user.profile?.picture;
}
if (user.profile?.picture.includes("bsky.network")) {
return user.profile?.picture;
}
return `${service}?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${user.profile?.picture}`;
} else {
return user.profile?.picture;

View File

@@ -51,9 +51,6 @@ const ColumnsLayoutSearchLazyImport = createFileRoute(
const ColumnsLayoutOnboardingLazyImport = createFileRoute(
'/columns/_layout/onboarding',
)()
const ColumnsLayoutLaunchpadLazyImport = createFileRoute(
'/columns/_layout/launchpad',
)()
const ColumnsLayoutUsersIdLazyImport = createFileRoute(
'/columns/_layout/users/$id',
)()
@@ -63,6 +60,9 @@ const ColumnsLayoutRepliesIdLazyImport = createFileRoute(
const ColumnsLayoutNotificationIdLazyImport = createFileRoute(
'/columns/_layout/notification/$id',
)()
const ColumnsLayoutLaunchpadIdLazyImport = createFileRoute(
'/columns/_layout/launchpad/$id',
)()
const ColumnsLayoutEventsIdLazyImport = createFileRoute(
'/columns/_layout/events/$id',
)()
@@ -171,15 +171,6 @@ const ColumnsLayoutOnboardingLazyRoute =
import('./routes/columns/_layout/onboarding.lazy').then((d) => d.Route),
)
const ColumnsLayoutLaunchpadLazyRoute = ColumnsLayoutLaunchpadLazyImport.update(
{
path: '/launchpad',
getParentRoute: () => ColumnsLayoutRoute,
} as any,
).lazy(() =>
import('./routes/columns/_layout/launchpad.lazy').then((d) => d.Route),
)
const SettingsIdWalletRoute = SettingsIdWalletImport.update({
path: '/wallet',
getParentRoute: () => SettingsIdLazyRoute,
@@ -245,6 +236,14 @@ const ColumnsLayoutNotificationIdLazyRoute =
),
)
const ColumnsLayoutLaunchpadIdLazyRoute =
ColumnsLayoutLaunchpadIdLazyImport.update({
path: '/launchpad/$id',
getParentRoute: () => ColumnsLayoutRoute,
} as any).lazy(() =>
import('./routes/columns/_layout/launchpad.$id.lazy').then((d) => d.Route),
)
const ColumnsLayoutEventsIdLazyRoute = ColumnsLayoutEventsIdLazyImport.update({
path: '/events/$id',
getParentRoute: () => ColumnsLayoutRoute,
@@ -436,13 +435,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsIdWalletImport
parentRoute: typeof SettingsIdLazyImport
}
'/columns/_layout/launchpad': {
id: '/columns/_layout/launchpad'
path: '/launchpad'
fullPath: '/columns/launchpad'
preLoaderRoute: typeof ColumnsLayoutLaunchpadLazyImport
parentRoute: typeof ColumnsLayoutImport
}
'/columns/_layout/onboarding': {
id: '/columns/_layout/onboarding'
path: '/onboarding'
@@ -513,6 +505,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ColumnsLayoutEventsIdLazyImport
parentRoute: typeof ColumnsLayoutImport
}
'/columns/_layout/launchpad/$id': {
id: '/columns/_layout/launchpad/$id'
path: '/launchpad/$id'
fullPath: '/columns/launchpad/$id'
preLoaderRoute: typeof ColumnsLayoutLaunchpadIdLazyImport
parentRoute: typeof ColumnsLayoutImport
}
'/columns/_layout/notification/$id': {
id: '/columns/_layout/notification/$id'
path: '/notification/$id'
@@ -569,7 +568,6 @@ const ColumnsLayoutCreateNewsfeedRouteWithChildren =
interface ColumnsLayoutRouteChildren {
ColumnsLayoutCreateNewsfeedRoute: typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
ColumnsLayoutGlobalRoute: typeof ColumnsLayoutGlobalRoute
ColumnsLayoutLaunchpadLazyRoute: typeof ColumnsLayoutLaunchpadLazyRoute
ColumnsLayoutOnboardingLazyRoute: typeof ColumnsLayoutOnboardingLazyRoute
ColumnsLayoutSearchLazyRoute: typeof ColumnsLayoutSearchLazyRoute
ColumnsLayoutTrendingLazyRoute: typeof ColumnsLayoutTrendingLazyRoute
@@ -578,6 +576,7 @@ interface ColumnsLayoutRouteChildren {
ColumnsLayoutNewsfeedIdRoute: typeof ColumnsLayoutNewsfeedIdRoute
ColumnsLayoutStoriesIdRoute: typeof ColumnsLayoutStoriesIdRoute
ColumnsLayoutEventsIdLazyRoute: typeof ColumnsLayoutEventsIdLazyRoute
ColumnsLayoutLaunchpadIdLazyRoute: typeof ColumnsLayoutLaunchpadIdLazyRoute
ColumnsLayoutNotificationIdLazyRoute: typeof ColumnsLayoutNotificationIdLazyRoute
ColumnsLayoutRepliesIdLazyRoute: typeof ColumnsLayoutRepliesIdLazyRoute
ColumnsLayoutUsersIdLazyRoute: typeof ColumnsLayoutUsersIdLazyRoute
@@ -587,7 +586,6 @@ const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = {
ColumnsLayoutCreateNewsfeedRoute:
ColumnsLayoutCreateNewsfeedRouteWithChildren,
ColumnsLayoutGlobalRoute: ColumnsLayoutGlobalRoute,
ColumnsLayoutLaunchpadLazyRoute: ColumnsLayoutLaunchpadLazyRoute,
ColumnsLayoutOnboardingLazyRoute: ColumnsLayoutOnboardingLazyRoute,
ColumnsLayoutSearchLazyRoute: ColumnsLayoutSearchLazyRoute,
ColumnsLayoutTrendingLazyRoute: ColumnsLayoutTrendingLazyRoute,
@@ -596,6 +594,7 @@ const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = {
ColumnsLayoutNewsfeedIdRoute: ColumnsLayoutNewsfeedIdRoute,
ColumnsLayoutStoriesIdRoute: ColumnsLayoutStoriesIdRoute,
ColumnsLayoutEventsIdLazyRoute: ColumnsLayoutEventsIdLazyRoute,
ColumnsLayoutLaunchpadIdLazyRoute: ColumnsLayoutLaunchpadIdLazyRoute,
ColumnsLayoutNotificationIdLazyRoute: ColumnsLayoutNotificationIdLazyRoute,
ColumnsLayoutRepliesIdLazyRoute: ColumnsLayoutRepliesIdLazyRoute,
ColumnsLayoutUsersIdLazyRoute: ColumnsLayoutUsersIdLazyRoute,
@@ -654,7 +653,6 @@ export interface FileRoutesByFullPath {
'/settings/$id/profile': typeof SettingsIdProfileRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute
'/settings/$id/wallet': typeof SettingsIdWalletRoute
'/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
@@ -665,6 +663,7 @@ export interface FileRoutesByFullPath {
'/columns/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute
'/columns/stories/$id': typeof ColumnsLayoutStoriesIdRoute
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
'/columns/launchpad/$id': typeof ColumnsLayoutLaunchpadIdLazyRoute
'/columns/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
@@ -689,7 +688,6 @@ export interface FileRoutesByTo {
'/settings/$id/profile': typeof SettingsIdProfileRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute
'/settings/$id/wallet': typeof SettingsIdWalletRoute
'/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
@@ -700,6 +698,7 @@ export interface FileRoutesByTo {
'/columns/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute
'/columns/stories/$id': typeof ColumnsLayoutStoriesIdRoute
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
'/columns/launchpad/$id': typeof ColumnsLayoutLaunchpadIdLazyRoute
'/columns/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
@@ -727,7 +726,6 @@ export interface FileRoutesById {
'/settings/$id/profile': typeof SettingsIdProfileRoute
'/settings/$id/relay': typeof SettingsIdRelayRoute
'/settings/$id/wallet': typeof SettingsIdWalletRoute
'/columns/_layout/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
'/columns/_layout/search': typeof ColumnsLayoutSearchLazyRoute
'/columns/_layout/trending': typeof ColumnsLayoutTrendingLazyRoute
@@ -738,6 +736,7 @@ export interface FileRoutesById {
'/columns/_layout/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute
'/columns/_layout/stories/$id': typeof ColumnsLayoutStoriesIdRoute
'/columns/_layout/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
'/columns/_layout/launchpad/$id': typeof ColumnsLayoutLaunchpadIdLazyRoute
'/columns/_layout/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute
'/columns/_layout/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
'/columns/_layout/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
@@ -765,7 +764,6 @@ export interface FileRouteTypes {
| '/settings/$id/profile'
| '/settings/$id/relay'
| '/settings/$id/wallet'
| '/columns/launchpad'
| '/columns/onboarding'
| '/columns/search'
| '/columns/trending'
@@ -776,6 +774,7 @@ export interface FileRouteTypes {
| '/columns/newsfeed/$id'
| '/columns/stories/$id'
| '/columns/events/$id'
| '/columns/launchpad/$id'
| '/columns/notification/$id'
| '/columns/replies/$id'
| '/columns/users/$id'
@@ -799,7 +798,6 @@ export interface FileRouteTypes {
| '/settings/$id/profile'
| '/settings/$id/relay'
| '/settings/$id/wallet'
| '/columns/launchpad'
| '/columns/onboarding'
| '/columns/search'
| '/columns/trending'
@@ -810,6 +808,7 @@ export interface FileRouteTypes {
| '/columns/newsfeed/$id'
| '/columns/stories/$id'
| '/columns/events/$id'
| '/columns/launchpad/$id'
| '/columns/notification/$id'
| '/columns/replies/$id'
| '/columns/users/$id'
@@ -835,7 +834,6 @@ export interface FileRouteTypes {
| '/settings/$id/profile'
| '/settings/$id/relay'
| '/settings/$id/wallet'
| '/columns/_layout/launchpad'
| '/columns/_layout/onboarding'
| '/columns/_layout/search'
| '/columns/_layout/trending'
@@ -846,6 +844,7 @@ export interface FileRouteTypes {
| '/columns/_layout/newsfeed/$id'
| '/columns/_layout/stories/$id'
| '/columns/_layout/events/$id'
| '/columns/_layout/launchpad/$id'
| '/columns/_layout/notification/$id'
| '/columns/_layout/replies/$id'
| '/columns/_layout/users/$id'
@@ -938,7 +937,6 @@ export const routeTree = rootRoute
"children": [
"/columns/_layout/create-newsfeed",
"/columns/_layout/global",
"/columns/_layout/launchpad",
"/columns/_layout/onboarding",
"/columns/_layout/search",
"/columns/_layout/trending",
@@ -947,6 +945,7 @@ export const routeTree = rootRoute
"/columns/_layout/newsfeed/$id",
"/columns/_layout/stories/$id",
"/columns/_layout/events/$id",
"/columns/_layout/launchpad/$id",
"/columns/_layout/notification/$id",
"/columns/_layout/replies/$id",
"/columns/_layout/users/$id"
@@ -1008,10 +1007,6 @@ export const routeTree = rootRoute
"filePath": "settings.$id/wallet.tsx",
"parent": "/settings/$id"
},
"/columns/_layout/launchpad": {
"filePath": "columns/_layout/launchpad.lazy.tsx",
"parent": "/columns/_layout"
},
"/columns/_layout/onboarding": {
"filePath": "columns/_layout/onboarding.lazy.tsx",
"parent": "/columns/_layout"
@@ -1052,6 +1047,10 @@ export const routeTree = rootRoute
"filePath": "columns/_layout/events.$id.lazy.tsx",
"parent": "/columns/_layout"
},
"/columns/_layout/launchpad/$id": {
"filePath": "columns/_layout/launchpad.$id.lazy.tsx",
"parent": "/columns/_layout"
},
"/columns/_layout/notification/$id": {
"filePath": "columns/_layout/notification.$id.lazy.tsx",
"parent": "/columns/_layout"

View File

@@ -60,26 +60,24 @@ function Topbar() {
data-tauri-drag-region
className="relative z-[200] flex-1 flex items-center justify-end gap-4"
>
{accounts?.length ? (
<div className="inline-flex items-center gap-2">
<button
type="button"
onClick={() => LumeWindow.openEditor()}
className="inline-flex items-center justify-center h-7 gap-1 px-2 text-sm font-medium bg-black/5 dark:bg-white/5 rounded-full w-max hover:bg-blue-500 hover:text-white"
>
<PublishIcon className="size-4" />
New Post
</button>
<button
type="button"
onClick={() => LumeWindow.openSearch()}
className="inline-flex items-center justify-center size-7 bg-black/5 dark:bg-white/5 rounded-full hover:bg-blue-500 hover:text-white"
>
<MagnifyingGlass className="size-4" />
</button>
</div>
) : null}
<div id="toolbar" className="inline-flex items-center gap-2" />
<div className="inline-flex items-center gap-2">
<button
type="button"
onClick={() => LumeWindow.openEditor()}
className="inline-flex items-center justify-center h-7 gap-1 px-2 text-sm font-medium bg-black/5 dark:bg-white/5 rounded-full w-max hover:bg-blue-500 hover:text-white"
>
<PublishIcon className="size-4" />
New Post
</button>
<button
type="button"
onClick={() => LumeWindow.openSearch()}
className="inline-flex items-center justify-center size-7 bg-black/5 dark:bg-white/5 rounded-full hover:bg-blue-500 hover:text-white"
>
<MagnifyingGlass className="size-4" />
</button>
</div>
<div id="toolbar" className="inline-flex items-center gap-1" />
</div>
</div>
);

View File

@@ -1,14 +1,15 @@
import { appColumns } from "@/commons";
import { commands } from "@/commands.gen";
import { appColumns, displayNpub } from "@/commons";
import { Column, Spinner } from "@/components";
import { LumeWindow } from "@/system";
import type { ColumnEvent, LumeColumn } from "@/types";
import { ArrowLeft, ArrowRight, Plus, StackPlus } from "@phosphor-icons/react";
import type { ColumnEvent, LumeColumn, Metadata } from "@/types";
import { ArrowLeft, ArrowRight, Plus } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useStore } from "@tanstack/react-store";
import { listen } from "@tauri-apps/api/event";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { getCurrentWindow } from "@tauri-apps/api/window";
import useEmblaCarousel from "embla-carousel-react";
import { nanoid } from "nanoid";
import {
type ReactNode,
useCallback,
@@ -45,11 +46,14 @@ function Screen() {
}, []);
const add = useDebouncedCallback((column: LumeColumn) => {
column.label = `${column.label}-${nanoid()}`; // update col label
appColumns.setState((prev) => [column, ...prev]);
const exist = columns.find((col) => col.label === column.label);
if (emblaApi) {
emblaApi.scrollTo(0, true);
if (!exist) {
appColumns.setState((prev) => [column, ...prev]);
if (emblaApi) {
emblaApi.scrollTo(0, true);
}
}
}, 150);
@@ -141,47 +145,79 @@ function Screen() {
<Column key={column.label} column={column} />
))
)}
<div className="shrink-0 p-2 h-full w-[440px]">
<div className="size-full flex items-center justify-center">
<button
type="button"
onClick={() => LumeWindow.openLaunchpad()}
className="inline-flex items-center justify-center gap-1 rounded-full text-sm font-medium h-8 w-max pl-2 pr-3 bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10"
>
<Plus className="size-4" />
Add Column
</button>
</div>
</div>
<OpenLaunchpad />
</div>
</div>
<Toolbar>
<button
type="button"
onClick={() => LumeWindow.openLaunchpad()}
className="inline-flex items-center justify-center rounded-full size-7 hover:bg-black/5 dark:hover:bg-white/5"
>
<StackPlus className="size-4" />
</button>
<button
type="button"
onClick={() => scrollPrev()}
className="inline-flex items-center justify-center rounded-full size-7 hover:bg-black/5 dark:hover:bg-white/5"
>
<ArrowLeft className="size-4" />
<ArrowLeft className="size-4" weight="bold" />
</button>
<button
type="button"
onClick={() => scrollNext()}
className="inline-flex items-center justify-center rounded-full size-7 hover:bg-black/5 dark:hover:bg-white/5"
>
<ArrowRight className="size-4" />
<ArrowRight className="size-4" weight="bold" />
</button>
</Toolbar>
</div>
);
}
function OpenLaunchpad() {
const { accounts } = Route.useRouteContext();
const showContextMenu = useCallback(
async (e: React.MouseEvent) => {
e.preventDefault();
const list: Promise<MenuItem>[] = [];
for (const account of accounts) {
const res = await commands.getProfile(account);
let name = "unknown";
if (res.status === "ok") {
const profile: Metadata = JSON.parse(res.data);
name = profile.display_name ?? profile.name ?? "unknown";
}
list.push(
MenuItem.new({
text: `Open Launchpad for ${name} (${displayNpub(account, 16)})`,
action: () => LumeWindow.openLaunchpad(account),
}),
);
}
const items = await Promise.all(list);
const menu = await Menu.new({ items });
await menu.popup().catch((e) => console.error(e));
},
[accounts],
);
return (
<div className="shrink-0 p-2 h-full w-[440px]">
<div className="size-full flex items-center justify-center">
<button
type="button"
onClick={(e) => showContextMenu(e)}
className="inline-flex items-center justify-center gap-1 rounded-full text-sm font-medium h-8 w-max pl-2.5 pr-3 bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10"
>
<Plus className="size-3" weight="bold" />
Add Column
</button>
</div>
</div>
);
}
function Toolbar({ children }: { children: ReactNode[] }) {
const [domReady, setDomReady] = useState(false);

View File

@@ -1,26 +1,37 @@
import type { LumeColumn } from '@/types'
import { createFileRoute } from '@tanstack/react-router'
import { resolveResource } from '@tauri-apps/api/path'
import { readTextFile } from '@tauri-apps/plugin-fs'
import type { LumeColumn } from "@/types";
import { createFileRoute } from "@tanstack/react-router";
import { nanoid } from "nanoid";
export const Route = createFileRoute('/_app/')({
loader: async ({ context }) => {
const prevColumns = window.localStorage.getItem('columns')
export const Route = createFileRoute("/_app/")({
loader: async ({ context }) => {
const accounts = context.accounts;
const prevColumns = window.localStorage.getItem("columns");
if (!prevColumns) {
const resourcePath = await resolveResource('resources/columns.json')
const resourceFile = await readTextFile(resourcePath)
const content: LumeColumn[] = JSON.parse(resourceFile)
const initialAppColumns = content.filter((col) => col.default)
let initialAppColumns: LumeColumn[] = [];
return initialAppColumns
} else {
const parsed: LumeColumn[] = JSON.parse(prevColumns)
const initialAppColumns = parsed.filter((item) =>
item.account ? context.accounts.includes(item.account) : item,
)
if (!prevColumns || prevColumns.length < 1) {
initialAppColumns.push({
label: "onboarding",
name: "Onboarding",
url: "/columns/onboarding",
});
return initialAppColumns
}
},
})
for (const account of accounts) {
initialAppColumns.push({
label: `launchpad-${nanoid()}`,
name: "Launchpad",
url: `/columns/launchpad/${account}`,
account,
});
}
} else {
const parsed: LumeColumn[] = JSON.parse(prevColumns);
initialAppColumns = parsed.filter((item) =>
item.account ? context.accounts.includes(item.account) : item,
);
}
return initialAppColumns;
},
});

View File

@@ -4,19 +4,35 @@ import { Spinner, User } from "@/components";
import { LumeWindow } from "@/system";
import type { LumeColumn, NostrEvent } from "@/types";
import { ArrowClockwise, Plus } from "@phosphor-icons/react";
import * as Progress from "@radix-ui/react-progress";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { Channel } from "@tauri-apps/api/core";
import { resolveResource } from "@tauri-apps/api/path";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { nanoid } from "nanoid";
import { useCallback } from "react";
import { useCallback, useEffect, useState } from "react";
export const Route = createLazyFileRoute("/columns/_layout/launchpad")({
export const Route = createLazyFileRoute("/columns/_layout/launchpad/$id")({
component: Screen,
});
function Screen() {
const { id } = Route.useParams();
const { data: isSync } = useQuery({
queryKey: ["is-sync", id],
queryFn: async () => {
const res = await commands.isAccountSync(id);
if (res.status === "ok") {
return res.data;
} else {
return false;
}
},
});
return (
<ScrollArea.Root
type={"scroll"}
@@ -24,10 +40,15 @@ function Screen() {
className="overflow-hidden size-full"
>
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
<Groups />
<Interests />
<Accounts />
<Core />
{!isSync ? (
<SyncProgress />
) : (
<>
<Groups />
<Interests />
<Core />
</>
)}
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
@@ -40,8 +61,74 @@ function Screen() {
);
}
function SyncProgress() {
const { id } = Route.useParams();
const { queryClient } = Route.useRouteContext();
const [error, setError] = useState("");
const [progress, setProgress] = useState(0);
useEffect(() => {
(async () => {
if (progress >= 100) {
await queryClient.invalidateQueries();
}
})();
}, [progress]);
useEffect(() => {
const channel = new Channel<number>();
channel.onmessage = (message) => {
setProgress(message);
};
(async () => {
const res = await commands.syncAccount(id, channel);
if (res.status === "error") {
setError(res.error);
}
})();
}, []);
return (
<div className="size-full">
<div className="flex flex-col gap-3">
<div className="h-32 flex flex-col items-center justify-center rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800">
<div className="w-2/3 flex flex-col gap-2">
<Progress.Root
className="relative overflow-hidden bg-black/20 dark:bg-white/20 rounded-full w-full h-1"
style={{
transform: "translateZ(0)",
}}
value={progress}
>
<Progress.Indicator
className="bg-blue-500 size-full transition-transform duration-[660ms] ease-[cubic-bezier(0.65, 0, 0.35, 1)]"
style={{ transform: `translateX(-${100 - progress}%)` }}
/>
</Progress.Root>
<span className="text-center text-xs">
{error ? error : "Syncing in Progress..."}
</span>
</div>
</div>
<a
href="https://github.com/hoytech/strfry/blob/nextneg/docs/negentropy.md"
target="_blank"
className="text-center !underline text-xs font-medium text-blue-500"
rel="noreferrer"
>
Learn more about Negentropy
</a>
</div>
</div>
);
}
function Groups() {
const { isLoading, data, refetch, isRefetching } = useQuery({
const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({
queryKey: ["others", "groups"],
queryFn: async () => {
const res = await commands.getAllGroups();
@@ -72,7 +159,7 @@ function Groups() {
className="group 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="px-2 pt-2">
<div className="p-3 h-16 bg-neutral-100 rounded-lg flex flex-wrap items-center justify-center gap-2 overflow-y-auto">
<div className="p-3 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-lg flex flex-wrap items-center justify-center gap-2 overflow-y-auto">
{item.tags
.filter((tag) => tag[0] === "p")
.map((tag) => (
@@ -148,12 +235,16 @@ function Groups() {
<Spinner className="size-4" />
Loading...
</div>
) : !data.length ? (
) : 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?.length ? (
<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 groups yet.</p>
</div>
) : (
data.map((item) => renderItem(item))
data?.map((item) => renderItem(item))
)}
</div>
</div>
@@ -161,7 +252,7 @@ function Groups() {
}
function Interests() {
const { isLoading, data, refetch, isRefetching } = useQuery({
const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({
queryKey: ["others", "interests"],
queryFn: async () => {
const res = await commands.getAllInterests();
@@ -193,7 +284,7 @@ function Interests() {
className="group 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="px-2 pt-2">
<div className="p-3 h-16 bg-neutral-100 rounded-lg flex flex-wrap items-center justify-center gap-4 overflow-y-auto">
<div className="p-3 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-lg flex flex-wrap items-center justify-center gap-4 overflow-y-auto">
{item.tags
.filter((tag) => tag[0] === "t")
.map((tag) => (
@@ -267,87 +358,16 @@ function Interests() {
<Spinner className="size-4" />
Loading...
</div>
) : !data.length ? (
) : 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?.length ? (
<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 interests yet.</p>
</div>
) : (
data.map((item) => renderItem(item))
)}
</div>
</div>
);
}
function Accounts() {
const { isLoading, data: accounts } = useQuery({
queryKey: ["accounts"],
queryFn: async () => {
const res = await commands.getAccounts();
return res;
},
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">Accounts</h3>
</div>
<div className="flex flex-col gap-3">
{isLoading ? (
<div className="inline-flex items-center gap-1.5 text-sm">
<Spinner className="size-4" />
Loading...
</div>
) : (
accounts.map((account) => (
<div
key={account}
className="group 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="px-2 pt-2">
<User.Provider pubkey={account}>
<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 className="flex flex-col gap-2 p-2">
<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">Newsfeed</div>
<button
type="button"
onClick={() => LumeWindow.openNewsfeed(account)}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
<div 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">Stories</div>
<button
type="button"
onClick={() => LumeWindow.openStory(account)}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
<div 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">Notification</div>
<button
type="button"
onClick={() => LumeWindow.openNotification(account)}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
</div>
</div>
))
data?.map((item) => renderItem(item))
)}
</div>
</div>
@@ -355,8 +375,9 @@ function Accounts() {
}
function Core() {
const { isLoading, data } = useQuery({
queryKey: ["other-columns"],
const { id } = Route.useParams();
const { data } = useQuery({
queryKey: ["core-columns"],
queryFn: async () => {
const systemPath = "resources/columns.json";
const resourcePath = await resolveResource(systemPath);
@@ -373,38 +394,56 @@ function Core() {
return (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between px-2">
<h3 className="font-semibold">Others</h3>
<h3 className="font-semibold">Core</h3>
</div>
<div className="flex flex-col gap-3">
{isLoading ? (
<div className="inline-flex items-center gap-1.5 text-sm">
<Spinner className="size-4" />
Loading...
<div className="group 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">
<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">Newsfeed</div>
<button
type="button"
onClick={() => LumeWindow.openNewsfeed(id)}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
) : (
data.map((column) => (
<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">Stories</div>
<button
type="button"
onClick={() => LumeWindow.openStory(id)}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
<div 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">Notification</div>
<button
type="button"
onClick={() => LumeWindow.openNotification(id)}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
{data?.map((column) => (
<div
key={column.label}
className="group flex px-4 items-center justify-between h-16 rounded-xl bg-white dark:bg-black border-[.5px] border-neutral-300 dark:border-neutral-700"
className="px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800"
>
<div className="text-sm">
<div className="mb-px leading-tight font-semibold">
{column.name}
</div>
<div className="leading-tight text-neutral-500 dark:text-neutral-400">
{column.description}
</div>
</div>
<div className="text-sm font-medium">{column.name}</div>
<button
type="button"
onClick={() => LumeWindow.openColumn(column)}
className="text-xs font-semibold w-16 h-7 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
))
)}
))}
</div>
</div>
</div>
);

View File

@@ -52,7 +52,11 @@ function Screen() {
if (rootId) {
if (reactions.has(rootId)) {
reactions.get(rootId).push(event);
const ev = reactions.get(rootId);
if (ev) {
ev.push(event);
}
} else {
reactions.set(rootId, [event]);
}
@@ -64,7 +68,11 @@ function Screen() {
if (rootId) {
if (zaps.has(rootId)) {
zaps.get(rootId).push(event);
const ev = zaps.get(rootId);
if (ev) {
ev.push(event);
}
} else {
zaps.set(rootId, [event]);
}

View File

@@ -157,7 +157,7 @@ export class LumeEvent {
}
static async build(event: NostrEvent) {
const query = await commands.getEventMeta(event.content);
const query = await commands.getMetaFromEvent(event.content);
if (query.status === "ok") {
event.meta = query.data;

View File

@@ -1,6 +1,7 @@
import { commands } from "@/commands.gen";
import type { NostrEvent } from "@/types";
import { useQuery } from "@tanstack/react-query";
import { nip19 } from "nostr-tools";
import { LumeEvent } from "./event";
export function useEvent(id: string, repost?: string) {
@@ -10,7 +11,7 @@ export function useEvent(id: string, repost?: string) {
try {
if (repost?.length) {
const nostrEvent: NostrEvent = JSON.parse(repost);
const res = await commands.getEventMeta(nostrEvent.content);
const res = await commands.getMetaFromEvent(nostrEvent.content);
if (res.status === "ok") {
nostrEvent.meta = res.data;
@@ -19,12 +20,17 @@ export function useEvent(id: string, repost?: string) {
return new LumeEvent(nostrEvent);
}
// Validate ID
const normalizeId: string = id
.replace("nostr:", "")
.replace(/[^\w\s]/gi, "");
let normalizedId = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
const res = await commands.getEvent(normalizeId);
if (normalizedId.startsWith("nevent")) {
const decoded = nip19.decode(normalizedId);
if (decoded.type === "nevent") {
normalizedId = decoded.data.id;
}
}
const res = await commands.getEvent(normalizedId);
if (res.status === "ok") {
const data = res.data;

View File

@@ -16,17 +16,17 @@ export function useProfile(pubkey: string, embed?: string) {
return metadata;
}
let normalizeId = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
let normalizedId = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
if (normalizeId.startsWith("nprofile")) {
const decoded = nip19.decode(normalizeId);
if (normalizedId.startsWith("nprofile")) {
const decoded = nip19.decode(normalizedId);
if (decoded.type === "nprofile") {
normalizeId = decoded.data.pubkey;
normalizedId = decoded.data.pubkey;
}
}
const query = await commands.getProfile(normalizeId);
const query = await commands.getProfile(normalizedId);
if (query.status === "ok") {
return JSON.parse(query.data) as Metadata;

View File

@@ -11,13 +11,14 @@ export const LumeWindow = {
column,
});
},
openLaunchpad: async () => {
openLaunchpad: async (account: string) => {
await getCurrentWindow().emit("columns", {
type: "add",
column: {
label: "launchpad",
name: "Launchpad",
url: "/columns/launchpad",
url: `/columns/launchpad/${account}`,
account,
},
});
},