update
This commit is contained in:
20
src-tauri/Cargo.lock
generated
20
src-tauri/Cargo.lock
generated
@@ -3081,7 +3081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3480,7 +3480,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
[[package]]
|
||||
name = "nostr"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
|
||||
source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64 0.22.1",
|
||||
@@ -3510,7 +3510,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-database"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
|
||||
source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"flatbuffers",
|
||||
@@ -3524,7 +3524,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-lmdb"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
|
||||
source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f"
|
||||
dependencies = [
|
||||
"heed",
|
||||
"nostr",
|
||||
@@ -3537,7 +3537,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-relay-pool"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
|
||||
source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"async-wsocket",
|
||||
@@ -3555,7 +3555,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-sdk"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
|
||||
source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"atomic-destructor",
|
||||
@@ -3575,7 +3575,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-signer"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
|
||||
source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@@ -3588,7 +3588,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-zapper"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
|
||||
source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"nostr",
|
||||
@@ -3732,7 +3732,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nwc"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a"
|
||||
source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@@ -7223,7 +7223,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
wss://relay.damus.io,
|
||||
wss://relay.nostr.net,
|
||||
wss://relay.primal.net,
|
||||
wss://nostr.fmt.wiz.biz,
|
||||
|
||||
@@ -31,7 +31,28 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
|
||||
|
||||
Ok(RichEvent { raw, parsed })
|
||||
} else {
|
||||
Err("Event not found".to_string())
|
||||
match client
|
||||
.fetch_events(
|
||||
vec![Filter::new().id(event_id)],
|
||||
Some(Duration::from_secs(5)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.iter().next() {
|
||||
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("Event not found.".into())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
@@ -49,13 +70,16 @@ pub async fn get_meta_from_event(content: String) -> Result<Meta, ()> {
|
||||
pub async fn get_replies(id: String, state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Custom(1111)])
|
||||
.event(event_id);
|
||||
|
||||
match client
|
||||
.fetch_events(vec![filter], Some(Duration::from_secs(5)))
|
||||
.await
|
||||
{
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
Ok(events) => Ok(process_event(client, events, true).await),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -98,7 +122,7 @@ pub async fn get_all_events_by_author(
|
||||
.limit(limit as usize);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
Ok(events) => Ok(process_event(client, events, false).await),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -129,7 +153,7 @@ pub async fn get_all_events_by_authors(
|
||||
.authors(authors);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
Ok(events) => Ok(process_event(client, events, false).await),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -158,7 +182,7 @@ pub async fn get_all_events_by_hashtags(
|
||||
.fetch_events(vec![filter], Some(Duration::from_secs(5)))
|
||||
.await
|
||||
{
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
Ok(events) => Ok(process_event(client, events, false).await),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -182,7 +206,7 @@ pub async fn get_local_events(
|
||||
.until(as_of);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
Ok(events) => Ok(process_event(client, events, false).await),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -205,11 +229,8 @@ pub async fn get_global_events(
|
||||
.limit(FETCH_LIMIT)
|
||||
.until(as_of);
|
||||
|
||||
match client
|
||||
.fetch_events(vec![filter], Some(Duration::from_secs(5)))
|
||||
.await
|
||||
{
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(process_event(client, events, false).await),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -334,7 +355,13 @@ pub async fn is_reposted(id: String, state: State<'_, Nostr>) -> Result<bool, St
|
||||
|
||||
let authors: Vec<PublicKey> = accounts
|
||||
.iter()
|
||||
.map(|acc| PublicKey::from_str(acc).unwrap())
|
||||
.filter_map(|acc| {
|
||||
if let Ok(pk) = PublicKey::from_str(acc) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let filter = Filter::new()
|
||||
@@ -452,7 +479,7 @@ pub async fn search(query: String, state: State<'_, Nostr>) -> Result<Vec<RichEv
|
||||
let filter = Filter::new().search(query);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
Ok(events) => Ok(process_event(client, events, false).await),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,34 @@ pub async fn get_profile(id: String, state: State<'_, Nostr>) -> Result<String,
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
match client.database().profile(public_key).await {
|
||||
Ok(profile) => Ok(profile.metadata().as_json()),
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::Metadata)
|
||||
.limit(1);
|
||||
|
||||
match client.database().query(vec![filter.clone()]).await {
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.iter().next() {
|
||||
let metadata = Metadata::from_json(&event.content).map_err(|e| e.to_string())?;
|
||||
Ok(metadata.as_json())
|
||||
} else {
|
||||
match client
|
||||
.fetch_events(vec![filter], Some(Duration::from_secs(5)))
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.iter().next() {
|
||||
let metadata =
|
||||
Metadata::from_json(&event.content).map_err(|e| e.to_string())?;
|
||||
Ok(metadata.as_json())
|
||||
} else {
|
||||
Err("Profile not found.".into())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -203,7 +229,13 @@ pub async fn set_group(
|
||||
let client = &state.client;
|
||||
let public_keys: Vec<PublicKey> = users
|
||||
.iter()
|
||||
.map(|u| PublicKey::from_str(u).unwrap())
|
||||
.filter_map(|u| {
|
||||
if let Ok(pk) = PublicKey::from_str(u) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let label = title.to_lowercase().replace(" ", "-");
|
||||
let mut tags: Vec<Tag> = vec![Tag::title(title)];
|
||||
@@ -237,7 +269,7 @@ pub async fn set_group(
|
||||
.authors(public_keys)
|
||||
.limit(500);
|
||||
|
||||
if let Ok(report) = client.sync(filter, SyncOptions::default()).await {
|
||||
if let Ok(report) = client.sync(filter, &SyncOptions::default()).await {
|
||||
println!("Received: {}", report.received.len());
|
||||
handle.emit("synchronized", ()).unwrap();
|
||||
};
|
||||
@@ -283,7 +315,7 @@ pub async fn get_all_groups(state: State<'_, Nostr>) -> Result<Vec<RichEvent>, S
|
||||
let filter = Filter::new().kind(Kind::FollowSet).authors(authors);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
Ok(events) => Ok(process_event(client, events, false).await),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -331,7 +363,7 @@ pub async fn set_interest(
|
||||
.hashtags(hashtags)
|
||||
.limit(500);
|
||||
|
||||
if let Ok(report) = client.sync(filter, SyncOptions::default()).await {
|
||||
if let Ok(report) = client.sync(filter, &SyncOptions::default()).await {
|
||||
println!("Received: {}", report.received.len());
|
||||
handle.emit("synchronized", ()).unwrap();
|
||||
};
|
||||
@@ -381,7 +413,7 @@ pub async fn get_all_interests(state: State<'_, Nostr>) -> Result<Vec<RichEvent>
|
||||
.authors(authors);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
Ok(events) => Ok(process_event(client, events, false).await),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -560,14 +592,20 @@ 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,
|
||||
]);
|
||||
let filter = Filter::new()
|
||||
.pubkey(public_key)
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
Kind::Repost,
|
||||
Kind::Reaction,
|
||||
Kind::ZapReceipt,
|
||||
])
|
||||
.limit(200);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
match client
|
||||
.fetch_events(vec![filter], Some(Duration::from_secs(5)))
|
||||
.await
|
||||
{
|
||||
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::{self, File},
|
||||
str::FromStr,
|
||||
};
|
||||
@@ -52,20 +53,18 @@ pub fn sync_all(accounts: Vec<String>, app_handle: AppHandle) {
|
||||
//
|
||||
if let Ok(events) = client
|
||||
.database()
|
||||
.query(vec![Filter::new().kinds(vec![
|
||||
Kind::ContactList,
|
||||
Kind::FollowSet,
|
||||
Kind::MuteList,
|
||||
Kind::Repost,
|
||||
Kind::TextNote,
|
||||
])])
|
||||
.query(vec![Filter::new()
|
||||
.authors(public_keys)
|
||||
.kinds(vec![Kind::ContactList, Kind::FollowSet])])
|
||||
.await
|
||||
{
|
||||
let pubkeys: Vec<PublicKey> = events
|
||||
let set: HashSet<PublicKey> = events
|
||||
.iter()
|
||||
.flat_map(|ev| ev.tags.public_keys().copied())
|
||||
.collect();
|
||||
|
||||
let pubkeys: Vec<PublicKey> = set.into_iter().collect();
|
||||
|
||||
for chunk in pubkeys.chunks(500) {
|
||||
if chunk.is_empty() {
|
||||
break;
|
||||
@@ -78,10 +77,10 @@ pub fn sync_all(accounts: Vec<String>, app_handle: AppHandle) {
|
||||
let events = Filter::new()
|
||||
.authors(authors.clone())
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(1000);
|
||||
.limit(500);
|
||||
|
||||
if let Ok(output) = client
|
||||
.sync_with(&bootstrap_relays, events, SyncOptions::default())
|
||||
.sync_with(&bootstrap_relays, events, &SyncOptions::default())
|
||||
.await
|
||||
{
|
||||
NegentropyEvent {
|
||||
@@ -94,21 +93,20 @@ pub fn sync_all(accounts: Vec<String>, app_handle: AppHandle) {
|
||||
|
||||
// NEG: Sync metadata
|
||||
//
|
||||
let metadata = Filter::new()
|
||||
.authors(authors)
|
||||
let events = Filter::new()
|
||||
.authors(authors.clone())
|
||||
.kinds(vec![
|
||||
Kind::Metadata,
|
||||
Kind::ContactList,
|
||||
Kind::Interests,
|
||||
Kind::InterestSet,
|
||||
Kind::Interests,
|
||||
Kind::FollowSet,
|
||||
Kind::MuteList,
|
||||
Kind::RelaySet,
|
||||
Kind::EventDeletion,
|
||||
Kind::Custom(30315),
|
||||
])
|
||||
.limit(1000);
|
||||
.limit(500);
|
||||
|
||||
if let Ok(output) = client
|
||||
.sync_with(&bootstrap_relays, metadata, SyncOptions::default())
|
||||
.sync_with(&bootstrap_relays, events, &SyncOptions::default())
|
||||
.await
|
||||
{
|
||||
NegentropyEvent {
|
||||
@@ -120,62 +118,6 @@ pub fn sync_all(accounts: Vec<String>, app_handle: AppHandle) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NEG: Sync notification
|
||||
//
|
||||
let notification = Filter::new()
|
||||
.pubkeys(public_keys.clone())
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
Kind::Repost,
|
||||
Kind::Reaction,
|
||||
Kind::ZapReceipt,
|
||||
])
|
||||
.limit(500);
|
||||
|
||||
if let Ok(output) = client
|
||||
.sync_with(&bootstrap_relays, notification, SyncOptions::default())
|
||||
.await
|
||||
{
|
||||
NegentropyEvent {
|
||||
kind: NegentropyKind::Notification,
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,10 +177,7 @@ pub async fn sync_account(
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(output) = client
|
||||
.sync_with(&bootstrap_relays, filter, opts.clone())
|
||||
.await
|
||||
{
|
||||
if let Ok(output) = client.sync_with(&bootstrap_relays, filter, &opts).await {
|
||||
println!("Success: {:?}", output.success);
|
||||
println!("Failed: {:?}", output.failed);
|
||||
|
||||
@@ -279,37 +218,13 @@ pub async fn sync_account(
|
||||
])
|
||||
.limit(10000);
|
||||
|
||||
if let Ok(output) = client
|
||||
.sync_with(&bootstrap_relays, filter, opts.clone())
|
||||
.await
|
||||
{
|
||||
if let Ok(output) = client.sync_with(&bootstrap_relays, filter, &opts).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()
|
||||
|
||||
@@ -10,6 +10,7 @@ use tauri::window::Effect;
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl};
|
||||
use tauri::{WebviewBuilder, WebviewWindowBuilder};
|
||||
#[cfg(target_os = "windows")]
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
|
||||
#[derive(Serialize, Deserialize, Type)]
|
||||
@@ -35,9 +36,9 @@ pub struct Column {
|
||||
height: f32,
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
pub async fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
match app_handle.get_window("main") {
|
||||
Some(main_window) => match app_handle.get_webview(&column.label) {
|
||||
Some(_) => Ok(column.label),
|
||||
@@ -63,18 +64,18 @@ pub fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result<Str
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn close_column(label: String, app_handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
pub async fn close_column(label: String, app_handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => Ok(webview.close().is_ok()),
|
||||
None => Err("Cannot close, column not found.".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn update_column(
|
||||
pub async fn update_column(
|
||||
label: String,
|
||||
width: f32,
|
||||
height: f32,
|
||||
@@ -98,9 +99,9 @@ pub fn update_column(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
pub async fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => {
|
||||
webview.eval("location.reload(true)").unwrap();
|
||||
@@ -124,6 +125,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<Strin
|
||||
|
||||
Ok(current_window.label().to_string())
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
let new_window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
@@ -147,27 +149,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<Strin
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Restore native border
|
||||
new_window.add_border(None);
|
||||
|
||||
Ok(new_window.label().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
if let Some(current_window) = app_handle.get_window(&window.label) {
|
||||
if current_window.is_visible().unwrap_or_default() {
|
||||
let _ = current_window.set_focus();
|
||||
} else {
|
||||
let _ = current_window.show();
|
||||
let _ = current_window.set_focus();
|
||||
};
|
||||
|
||||
Ok(current_window.label().to_string())
|
||||
} else {
|
||||
#[cfg(target_os = "windows")]
|
||||
let new_window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
@@ -190,58 +172,14 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<Strin
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Set decoration
|
||||
new_window.create_overlay_titlebar().unwrap();
|
||||
|
||||
Ok(new_window.label().to_string())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn reopen_lume(app: tauri::AppHandle) {
|
||||
if let Some(window) = app.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(&app, app.config().app.windows.first().unwrap())
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Set decoration
|
||||
#[cfg(target_os = "windows")]
|
||||
window.create_overlay_titlebar().unwrap();
|
||||
new_window.create_overlay_titlebar().unwrap();
|
||||
|
||||
// Restore native border
|
||||
#[cfg(target_os = "macos")]
|
||||
window.add_border(None);
|
||||
new_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();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let win = window.clone();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::ThemeChanged(_) = event {
|
||||
win.set_traffic_lights_inset(7.0, 10.0).unwrap();
|
||||
}
|
||||
});
|
||||
Ok(new_window.label().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn quit() {
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
@@ -143,29 +143,33 @@ pub fn get_all_accounts() -> Vec<String> {
|
||||
accounts.into_iter().collect()
|
||||
}
|
||||
|
||||
pub async fn process_event(client: &Client, events: Events) -> Vec<RichEvent> {
|
||||
pub async fn process_event(client: &Client, events: Events, is_reply: bool) -> Vec<RichEvent> {
|
||||
// Remove event thread if event is TextNote
|
||||
let events: Vec<Event> = events
|
||||
.into_iter()
|
||||
.filter_map(|ev| {
|
||||
if ev.kind == Kind::TextNote {
|
||||
let tags = ev
|
||||
.tags
|
||||
.iter()
|
||||
.filter(|t| t.is_reply() || t.is_root())
|
||||
.filter_map(|t| t.content())
|
||||
.collect::<Vec<_>>();
|
||||
let events: Vec<Event> = if !is_reply {
|
||||
events
|
||||
.into_iter()
|
||||
.filter_map(|ev| {
|
||||
if ev.kind == Kind::TextNote {
|
||||
let tags = ev
|
||||
.tags
|
||||
.iter()
|
||||
.filter(|t| t.is_reply() || t.is_root())
|
||||
.filter_map(|t| t.content())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if tags.is_empty() {
|
||||
Some(ev)
|
||||
if tags.is_empty() {
|
||||
Some(ev)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
Some(ev)
|
||||
}
|
||||
} else {
|
||||
Some(ev)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
events.into_iter().collect()
|
||||
};
|
||||
|
||||
// Get deletion request by event's authors
|
||||
let ids: Vec<EventId> = events.iter().map(|ev| ev.id).collect();
|
||||
|
||||
@@ -156,8 +156,6 @@ fn main() {
|
||||
reload_column,
|
||||
close_column,
|
||||
open_window,
|
||||
reopen_lume,
|
||||
quit
|
||||
])
|
||||
.events(collect_events![Subscription, NegentropyEvent]);
|
||||
|
||||
@@ -207,12 +205,12 @@ fn main() {
|
||||
// Config
|
||||
let opts = Options::new()
|
||||
.gossip(true)
|
||||
.max_avg_latency(Duration::from_millis(800))
|
||||
.max_avg_latency(Duration::from_millis(500))
|
||||
.automatic_authentication(false)
|
||||
.connection_timeout(Some(Duration::from_secs(20)))
|
||||
.send_timeout(Some(Duration::from_secs(10)))
|
||||
.wait_for_send(false)
|
||||
.timeout(Duration::from_secs(20));
|
||||
.timeout(Duration::from_secs(12));
|
||||
|
||||
// Setup nostr client
|
||||
let client = ClientBuilder::default()
|
||||
@@ -370,29 +368,31 @@ fn main() {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle_clone.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
let accounts = state.accounts.lock().unwrap().clone();
|
||||
let accounts = get_all_accounts();
|
||||
|
||||
let public_keys: Vec<PublicKey> = accounts
|
||||
.iter()
|
||||
.filter_map(|acc| {
|
||||
if let Ok(pk) = PublicKey::from_str(acc) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !accounts.is_empty() {
|
||||
let public_keys: Vec<PublicKey> = accounts
|
||||
.iter()
|
||||
.filter_map(|acc| {
|
||||
if let Ok(pk) = PublicKey::from_str(acc) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Subscribe for new notification
|
||||
if let Ok(e) = client
|
||||
.subscribe_with_id(
|
||||
SubscriptionId::new(NOTIFICATION_SUB_ID),
|
||||
vec![Filter::new().pubkeys(public_keys).since(Timestamp::now())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
println!("Subscribed for notification on {} relays", e.success.len())
|
||||
// Subscribe for new notification
|
||||
if let Ok(e) = client
|
||||
.subscribe_with_id(
|
||||
SubscriptionId::new(NOTIFICATION_SUB_ID),
|
||||
vec![Filter::new().pubkeys(public_keys).since(Timestamp::now())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
println!("Subscribed for notification on {} relays", e.success.len())
|
||||
}
|
||||
}
|
||||
|
||||
let allow_notification = match handle_clone.notification().request_permission() {
|
||||
@@ -408,7 +408,6 @@ fn main() {
|
||||
|
||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
||||
let mut notifications = client.pool().notifications();
|
||||
let mut new_events: Vec<EventId> = Vec::new();
|
||||
|
||||
while let Ok(notification) = notifications.recv().await {
|
||||
match notification {
|
||||
@@ -426,17 +425,6 @@ fn main() {
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/rust-nostr/nostr/issues/509
|
||||
// TODO: remove
|
||||
let _ = client
|
||||
.fetch_events(
|
||||
vec![Filter::new()
|
||||
.kind(Kind::TextNote)
|
||||
.limit(0)],
|
||||
Some(Duration::from_secs(5)),
|
||||
)
|
||||
.await;
|
||||
|
||||
if allow_notification {
|
||||
if let Err(e) = &handle_clone
|
||||
.notification()
|
||||
@@ -469,22 +457,8 @@ fn main() {
|
||||
event,
|
||||
} = message
|
||||
{
|
||||
let tags: Vec<String> = event
|
||||
.tags
|
||||
.public_keys()
|
||||
.filter_map(|pk| {
|
||||
if let Ok(bech32) = pk.to_bech32() {
|
||||
Some(bech32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Handle events from notification subscription
|
||||
if subscription_id == notification_id
|
||||
&& tags.iter().any(|item| accounts.iter().any(|i| i == item))
|
||||
{
|
||||
if subscription_id == notification_id {
|
||||
// Send native notification
|
||||
if allow_notification {
|
||||
let author = client
|
||||
@@ -501,37 +475,22 @@ fn main() {
|
||||
&handle_clone,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let payload = RichEvent {
|
||||
raw: event.as_json(),
|
||||
parsed: if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
let payload = RichEvent {
|
||||
raw: event.as_json(),
|
||||
parsed: if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
handle_clone
|
||||
.emit_to(
|
||||
if let Err(e) = handle_clone.emit_to(
|
||||
EventTarget::labeled(subscription_id.to_string()),
|
||||
"event",
|
||||
payload,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if state
|
||||
.subscriptions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|i| i == &subscription_id)
|
||||
{
|
||||
new_events.push(event.id);
|
||||
|
||||
if new_events.len() > 5 {
|
||||
handle_clone.emit("synchronized", ()).unwrap();
|
||||
new_events.clear();
|
||||
) {
|
||||
println!("Emitter error: {}", e)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
58
src/app.tsx
58
src/app.tsx
@@ -1,31 +1,24 @@
|
||||
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { createStore } from "@tauri-apps/plugin-store";
|
||||
import { StrictMode } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { newQueryStorage } from "./commons";
|
||||
import type { LumeEvent } from "./system";
|
||||
|
||||
import { routeTree } from "./routes.gen"; // auto generated file
|
||||
import "./app.css"; // global styles
|
||||
|
||||
const platform = type();
|
||||
// @ts-expect-error, required: https://github.com/tauri-apps/plugins-workspace/pull/1860
|
||||
const store = await createStore(".cache", { autoSave: 250 });
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
gcTime: 1000 * 30,
|
||||
persister: experimental_createPersister({
|
||||
storage: newQueryStorage(store),
|
||||
maxAge: 1000 * 60 * 60 * 12,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
interface HistoryState {
|
||||
events?: LumeEvent[];
|
||||
}
|
||||
}
|
||||
|
||||
const platform = type();
|
||||
const queryClient = new QueryClient();
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient, platform },
|
||||
@@ -36,28 +29,11 @@ const router = createRouter({
|
||||
},
|
||||
});
|
||||
|
||||
// Register things for typesafety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
interface HistoryState {
|
||||
events?: LumeEvent[];
|
||||
}
|
||||
}
|
||||
const rootElement = document.getElementById("root");
|
||||
const root = ReactDOM.createRoot(rootElement as unknown as HTMLElement);
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: idk
|
||||
const rootElement = document.getElementById("root")!;
|
||||
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -495,12 +495,6 @@ async openWindow(window: Window) : Promise<Result<string, string>> {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async reopenLume() : Promise<void> {
|
||||
await TAURI_INVOKE("reopen_lume");
|
||||
},
|
||||
async quit() : Promise<void> {
|
||||
await TAURI_INVOKE("quit");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,20 @@ import type { LumeColumn } from "@/types";
|
||||
import { CaretDown, Check } from "@phosphor-icons/react";
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { User } from "./user";
|
||||
|
||||
export function Column({ column }: { column: LumeColumn }) {
|
||||
const webviewLabel = useMemo(() => `column-${column.label}`, [column.label]);
|
||||
|
||||
const [rect, ref] = useRect();
|
||||
const [_error, setError] = useState<string>(null);
|
||||
const [_error, setError] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -33,10 +39,8 @@ export function Column({ column }: { column: LumeColumn }) {
|
||||
})();
|
||||
}, [rect]);
|
||||
|
||||
useEffect(() => {
|
||||
const isCreated = window.sessionStorage.getItem(webviewLabel);
|
||||
|
||||
if (!isCreated) {
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current) {
|
||||
const initialRect = ref.current.getBoundingClientRect();
|
||||
|
||||
commands
|
||||
@@ -51,7 +55,6 @@ export function Column({ column }: { column: LumeColumn }) {
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
console.log("webview is created: ", webviewLabel);
|
||||
window.sessionStorage.setItem(webviewLabel, "");
|
||||
} else {
|
||||
setError(res.error);
|
||||
}
|
||||
@@ -172,7 +175,11 @@ function Header({
|
||||
<div
|
||||
contentEditable
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(e) => setTitle(e.currentTarget.textContent)}
|
||||
onBlur={(e) => {
|
||||
if (e.currentTarget.textContent) {
|
||||
setTitle(e.currentTarget.textContent);
|
||||
}
|
||||
}}
|
||||
className="text-[12px] font-semibold focus:outline-none"
|
||||
>
|
||||
{name}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export function Hashtag({ tag }: { tag: string }) {
|
||||
return (
|
||||
<span className="leading-normal cursor-default text-blue-500 hover:text-blue-600 font-normal">
|
||||
{tag}
|
||||
{tag.includes("#") ? tag : `#${tag}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { type RefObject, useEffect, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/events/$id")({
|
||||
@@ -35,7 +35,7 @@ function Screen() {
|
||||
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"
|
||||
>
|
||||
<Virtualizer scrollRef={ref}>
|
||||
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
|
||||
<RootEvent />
|
||||
<ReplyList />
|
||||
</Virtualizer>
|
||||
@@ -64,10 +64,10 @@ function RootEvent() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (isError || !event) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm text-red-500">
|
||||
{error.message}
|
||||
{error?.message || "Event not found within your current relay set"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -181,15 +181,21 @@ function ReplyList() {
|
||||
|
||||
useEffect(() => {
|
||||
events.subscription
|
||||
.emit({ label, kind: "Subscribe", event_id: id })
|
||||
.emit({
|
||||
label: label || id,
|
||||
kind: "Subscribe",
|
||||
event_id: id,
|
||||
contacts: null,
|
||||
})
|
||||
.then(() => console.log("Subscribe: ", label));
|
||||
|
||||
return () => {
|
||||
events.subscription
|
||||
.emit({
|
||||
label,
|
||||
label: label || id,
|
||||
kind: "Unsubscribe",
|
||||
event_id: id,
|
||||
contacts: null,
|
||||
})
|
||||
.then(() => console.log("Unsubscribe: ", label));
|
||||
};
|
||||
@@ -229,7 +235,7 @@ function ReplyList() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
{!data.length ? (
|
||||
{!data?.length ? (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-4">
|
||||
<h3 className="text-3xl">👋</h3>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ArrowDown } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { type RefObject, useCallback, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createFileRoute("/columns/_layout/global")({
|
||||
@@ -27,7 +27,7 @@ export function Screen() {
|
||||
queryKey: ["events", "global", label],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
const until = pageParam > 0 ? pageParam.toString() : null;
|
||||
const res = await commands.getGlobalEvents(until);
|
||||
|
||||
if (res.status === "error") {
|
||||
@@ -36,7 +36,13 @@ export function Screen() {
|
||||
|
||||
return toLumeEvents(res.data);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
if (lastEvent) {
|
||||
return lastEvent.created_at - 1;
|
||||
}
|
||||
},
|
||||
select: (data) => data?.pages.flat(),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
@@ -47,14 +53,17 @@ export function Screen() {
|
||||
(event: LumeEvent) => {
|
||||
if (!event) return;
|
||||
switch (event.kind) {
|
||||
case Kind.Repost:
|
||||
case Kind.Repost: {
|
||||
const repostId = event.repostId;
|
||||
|
||||
return (
|
||||
<RepostNote
|
||||
key={event.id}
|
||||
key={repostId + event.id}
|
||||
event={event}
|
||||
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<TextNote
|
||||
@@ -76,9 +85,9 @@ export function Screen() {
|
||||
>
|
||||
<ScrollArea.Viewport
|
||||
ref={ref}
|
||||
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
className="relative h-full bg-white dark:bg-neutral-800 rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<Virtualizer scrollRef={ref}>
|
||||
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
|
||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||
<div className="w-max h-8 pl-2 pr-3 inline-flex items-center justify-center gap-1.5 rounded-full shadow-lg text-sm font-medium text-white bg-black dark:text-black dark:bg-white">
|
||||
@@ -92,7 +101,7 @@ export function Screen() {
|
||||
<Spinner className="size-4" />
|
||||
<span className="text-sm font-medium">Loading...</span>
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
) : !data?.length ? (
|
||||
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||
🎉 Yo. You're catching up on all latest notes.
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ArrowDown } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { type RefObject, useCallback, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/groups/$id")({
|
||||
@@ -29,7 +29,7 @@ export function Screen() {
|
||||
queryKey: ["events", "groups", params.id],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
const until = pageParam > 0 ? pageParam.toString() : null;
|
||||
const res = await commands.getAllEventsByAuthors(group, until);
|
||||
|
||||
if (res.status === "error") {
|
||||
@@ -38,7 +38,13 @@ export function Screen() {
|
||||
|
||||
return toLumeEvents(res.data);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
if (lastEvent) {
|
||||
return lastEvent.created_at - 1;
|
||||
}
|
||||
},
|
||||
select: (data) => data?.pages.flat(),
|
||||
enabled: group?.length > 0,
|
||||
refetchOnWindowFocus: false,
|
||||
@@ -50,14 +56,17 @@ export function Screen() {
|
||||
(event: LumeEvent) => {
|
||||
if (!event) return;
|
||||
switch (event.kind) {
|
||||
case Kind.Repost:
|
||||
case Kind.Repost: {
|
||||
const repostId = event.repostId;
|
||||
|
||||
return (
|
||||
<RepostNote
|
||||
key={event.id}
|
||||
key={repostId + event.id}
|
||||
event={event}
|
||||
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<TextNote
|
||||
@@ -81,7 +90,7 @@ export function Screen() {
|
||||
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"
|
||||
>
|
||||
<Virtualizer scrollRef={ref}>
|
||||
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
|
||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||
<div className="w-max h-8 pl-2 pr-3 inline-flex items-center justify-center gap-1.5 rounded-full shadow-lg text-sm font-medium text-white bg-black dark:text-black dark:bg-white">
|
||||
@@ -95,7 +104,7 @@ export function Screen() {
|
||||
<Spinner className="size-4" />
|
||||
<span className="text-sm font-medium">Loading...</span>
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
) : !data?.length ? (
|
||||
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||
🎉 Yo. You're catching up on all latest notes.
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ArrowDown } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { type RefObject, useCallback, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/interests/$id")({
|
||||
@@ -30,7 +30,7 @@ export function Screen() {
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
const tags = hashtags.map((tag) => tag.toLowerCase().replace("#", ""));
|
||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
const until = pageParam > 0 ? pageParam.toString() : null;
|
||||
const res = await commands.getAllEventsByHashtags(tags, until);
|
||||
|
||||
if (res.status === "error") {
|
||||
@@ -39,7 +39,13 @@ export function Screen() {
|
||||
|
||||
return toLumeEvents(res.data);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
if (lastEvent) {
|
||||
return lastEvent.created_at - 1;
|
||||
}
|
||||
},
|
||||
select: (data) => data?.pages.flat(),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
@@ -50,14 +56,17 @@ export function Screen() {
|
||||
(event: LumeEvent) => {
|
||||
if (!event) return;
|
||||
switch (event.kind) {
|
||||
case Kind.Repost:
|
||||
case Kind.Repost: {
|
||||
const repostId = event.repostId;
|
||||
|
||||
return (
|
||||
<RepostNote
|
||||
key={event.id}
|
||||
key={repostId + event.id}
|
||||
event={event}
|
||||
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<TextNote
|
||||
@@ -79,9 +88,9 @@ export function Screen() {
|
||||
>
|
||||
<ScrollArea.Viewport
|
||||
ref={ref}
|
||||
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
className="relative h-full bg-white dark:bg-neutral-800 rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<Virtualizer scrollRef={ref}>
|
||||
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
|
||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||
<div className="w-max h-8 pl-2 pr-3 inline-flex items-center justify-center gap-1.5 rounded-full shadow-lg text-sm font-medium text-white bg-black dark:text-black dark:bg-white">
|
||||
@@ -95,7 +104,7 @@ export function Screen() {
|
||||
<Spinner className="size-4" />
|
||||
<span className="text-sm font-medium">Loading...</span>
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
) : !data?.length ? (
|
||||
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||
🎉 Yo. You're catching up on all latest notes.
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const Route = createLazyFileRoute("/columns/_layout/launchpad/$id")({
|
||||
|
||||
function Screen() {
|
||||
const { id } = Route.useParams();
|
||||
const { data: isSync } = useQuery({
|
||||
const { isLoading, data: isSync } = useQuery({
|
||||
queryKey: ["is-sync", id],
|
||||
queryFn: async () => {
|
||||
const res = await commands.isAccountSync(id);
|
||||
@@ -40,7 +40,9 @@ function Screen() {
|
||||
className="overflow-hidden size-full"
|
||||
>
|
||||
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
|
||||
{!isSync ? (
|
||||
{isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : !isSync ? (
|
||||
<SyncProgress />
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -7,7 +7,8 @@ import { ArrowDown } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { type RefObject, useCallback, useEffect, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/newsfeed/$id")({
|
||||
@@ -17,7 +18,7 @@ export const Route = createLazyFileRoute("/columns/_layout/newsfeed/$id")({
|
||||
export function Screen() {
|
||||
const contacts = Route.useLoaderData();
|
||||
const search = Route.useSearch();
|
||||
|
||||
const { queryClient } = Route.useRouteContext();
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
@@ -38,7 +39,13 @@ export function Screen() {
|
||||
|
||||
return toLumeEvents(res.data);
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.at?.(-1)?.created_at - 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
|
||||
if (lastEvent) {
|
||||
return lastEvent.created_at - 1;
|
||||
}
|
||||
},
|
||||
select: (data) => data?.pages.flat(),
|
||||
enabled: contacts?.length > 0,
|
||||
});
|
||||
@@ -52,14 +59,17 @@ export function Screen() {
|
||||
}
|
||||
|
||||
switch (event.kind) {
|
||||
case Kind.Repost:
|
||||
case Kind.Repost: {
|
||||
const repostId = event.repostId;
|
||||
|
||||
return (
|
||||
<RepostNote
|
||||
key={event.id}
|
||||
key={repostId + event.id}
|
||||
event={event}
|
||||
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<TextNote
|
||||
@@ -95,6 +105,18 @@ export function Screen() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen("synchronized", async () => {
|
||||
await queryClient.refetchQueries({
|
||||
queryKey: ["events", "newsfeed", search.label],
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((f) => f());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ScrollArea.Root
|
||||
type={"scroll"}
|
||||
@@ -103,9 +125,9 @@ export function Screen() {
|
||||
>
|
||||
<ScrollArea.Viewport
|
||||
ref={ref}
|
||||
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
className="relative h-full bg-white dark:bg-neutral-800 rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<Virtualizer scrollRef={ref}>
|
||||
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
|
||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||
<div className="w-max h-8 pl-2 pr-3 inline-flex items-center justify-center gap-1.5 rounded-full shadow-lg text-sm font-medium text-white bg-black dark:text-black dark:bg-white">
|
||||
@@ -119,7 +141,7 @@ export function Screen() {
|
||||
<Spinner className="size-4" />
|
||||
<span className="text-sm font-medium">Loading...</span>
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
) : !data?.length ? (
|
||||
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||
🎉 Yo. You're catching up on all latest notes.
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,13 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { type ReactNode, useEffect, useMemo, useRef } from "react";
|
||||
import {
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/notification/$id")({
|
||||
@@ -110,7 +116,7 @@ function Screen() {
|
||||
<div className="px-3 h-full overflow-y-auto">
|
||||
<Tabs.Root
|
||||
defaultValue="replies"
|
||||
className="flex flex-col 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="flex flex-col 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"
|
||||
>
|
||||
<Tabs.List className="h-11 shrink-0 flex items-center">
|
||||
<Tabs.Trigger
|
||||
@@ -138,62 +144,74 @@ function Screen() {
|
||||
className="min-h-0 flex-1 overflow-x-hidden"
|
||||
>
|
||||
<Tab value="replies">
|
||||
{data.texts.map((event) => (
|
||||
<TextNote key={event.id} event={event} />
|
||||
))}
|
||||
{!data?.texts ? (
|
||||
<div>Empty</div>
|
||||
) : (
|
||||
data.texts.map((event) => (
|
||||
<TextNote key={event.id} event={event} />
|
||||
))
|
||||
)}
|
||||
</Tab>
|
||||
<Tab value="reactions">
|
||||
{[...data.reactions.entries()].map(([root, events]) => (
|
||||
<div
|
||||
key={root}
|
||||
className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
|
||||
>
|
||||
<div className="flex flex-col flex-1 min-w-0 gap-2">
|
||||
<div className="flex items-center gap-2 pb-2">
|
||||
<RootNote id={root} />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{events.map((event) => (
|
||||
<User.Provider key={event.id} pubkey={event.pubkey}>
|
||||
<User.Root className="shrink-0 flex rounded-full h-7 bg-neutral-100 dark:bg-neutral-900 p-[2px]">
|
||||
<User.Avatar className="flex-1 rounded-full size-6" />
|
||||
<div className="inline-flex items-center justify-center flex-1 text-xs truncate rounded-full size-6">
|
||||
{event.kind === Kind.Reaction ? (
|
||||
event.content === "+" ? (
|
||||
"👍"
|
||||
{!data?.reactions ? (
|
||||
<div>Empty</div>
|
||||
) : (
|
||||
[...data.reactions.entries()].map(([root, events]) => (
|
||||
<div
|
||||
key={root}
|
||||
className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
|
||||
>
|
||||
<div className="flex flex-col flex-1 min-w-0 gap-2">
|
||||
<div className="flex items-center gap-2 pb-2">
|
||||
<RootNote id={root} />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{events.map((event) => (
|
||||
<User.Provider key={event.id} pubkey={event.pubkey}>
|
||||
<User.Root className="shrink-0 flex rounded-full h-7 bg-neutral-100 dark:bg-neutral-900 p-[2px]">
|
||||
<User.Avatar className="flex-1 rounded-full size-6" />
|
||||
<div className="inline-flex items-center justify-center flex-1 text-xs truncate rounded-full size-6">
|
||||
{event.kind === Kind.Reaction ? (
|
||||
event.content === "+" ? (
|
||||
"👍"
|
||||
) : (
|
||||
event.content
|
||||
)
|
||||
) : (
|
||||
event.content
|
||||
)
|
||||
) : (
|
||||
<RepostIcon className="text-teal-400 size-4 dark:text-teal-600" />
|
||||
)}
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
))}
|
||||
<RepostIcon className="text-teal-400 size-4 dark:text-teal-600" />
|
||||
)}
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</Tab>
|
||||
<Tab value="zaps">
|
||||
{[...data.zaps.entries()].map(([root, events]) => (
|
||||
<div
|
||||
key={root}
|
||||
className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
|
||||
>
|
||||
<div className="flex flex-col flex-1 min-w-0 gap-2">
|
||||
<div className="flex items-center gap-2 pb-2">
|
||||
<RootNote id={root} />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{events.map((event) => (
|
||||
<ZapReceipt key={event.id} event={event} />
|
||||
))}
|
||||
{!data?.zaps ? (
|
||||
<div>Empty</div>
|
||||
) : (
|
||||
[...data.zaps.entries()].map(([root, events]) => (
|
||||
<div
|
||||
key={root}
|
||||
className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
|
||||
>
|
||||
<div className="flex flex-col flex-1 min-w-0 gap-2">
|
||||
<div className="flex items-center gap-2 pb-2">
|
||||
<RootNote id={root} />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{events.map((event) => (
|
||||
<ZapReceipt key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</Tab>
|
||||
<ScrollArea.Scrollbar
|
||||
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||
@@ -208,13 +226,15 @@ function Screen() {
|
||||
);
|
||||
}
|
||||
|
||||
function Tab({ value, children }: { value: string; children: ReactNode[] }) {
|
||||
function Tab({ value, children }: { value: string; children: ReactNode }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<Tabs.Content value={value} className="size-full">
|
||||
<ScrollArea.Viewport ref={ref} className="h-full">
|
||||
<Virtualizer scrollRef={ref}>{children}</Virtualizer>
|
||||
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
|
||||
{children}
|
||||
</Virtualizer>
|
||||
</ScrollArea.Viewport>
|
||||
</Tabs.Content>
|
||||
);
|
||||
@@ -311,10 +331,16 @@ function TextNote({ event }: { event: LumeEvent }) {
|
||||
}
|
||||
|
||||
function ZapReceipt({ event }: { event: LumeEvent }) {
|
||||
const amount = useMemo(
|
||||
() => decodeZapInvoice(event.tags).bitcoinFormatted ?? "0",
|
||||
[event.id],
|
||||
);
|
||||
const amount = useMemo(() => {
|
||||
const decoded = decodeZapInvoice(event.tags);
|
||||
|
||||
if (decoded) {
|
||||
return decoded.bitcoinFormatted;
|
||||
} else {
|
||||
return "0";
|
||||
}
|
||||
}, [event.id]);
|
||||
|
||||
const sender = useMemo(
|
||||
() => event.tags.find((tag) => tag[0] === "P")?.[1],
|
||||
[event.id],
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useRouter,
|
||||
useRouterState,
|
||||
} from "@tanstack/react-router";
|
||||
import { useRef } from "react";
|
||||
import { type RefObject, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/replies/$id")({
|
||||
@@ -20,7 +20,7 @@ function Screen() {
|
||||
|
||||
return (
|
||||
<div className="px-3 h-full">
|
||||
<div className="size-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">
|
||||
<div className="size-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">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="h-10 shrink-0 border-b border-black/5 dark:border-white/5 flex items-center justify-between px-2">
|
||||
<button
|
||||
@@ -38,10 +38,14 @@ function Screen() {
|
||||
className="overflow-hidden size-full flex-1"
|
||||
>
|
||||
<ScrollArea.Viewport ref={ref} className="h-full p-3">
|
||||
<Virtualizer scrollRef={ref}>
|
||||
{events.map((event) => (
|
||||
<ReplyNote key={event.id} event={event} />
|
||||
))}
|
||||
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
|
||||
{!events ? (
|
||||
<div>Empty</div>
|
||||
) : (
|
||||
events.map((event) => (
|
||||
<ReplyNote key={event.id} event={event} />
|
||||
))
|
||||
)}
|
||||
</Virtualizer>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar
|
||||
|
||||
@@ -7,7 +7,13 @@ import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useCallback, useRef, useState, useTransition } from "react";
|
||||
import {
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/search")({
|
||||
@@ -97,7 +103,7 @@ function Screen() {
|
||||
|
||||
return (
|
||||
<div className="px-3 h-full">
|
||||
<div className="flex flex-col gap-3 size-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">
|
||||
<div className="flex flex-col gap-3 size-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">
|
||||
<div className="h-9 shrink-0 pt-6 px-3 flex items-center gap-2">
|
||||
<input
|
||||
name="search"
|
||||
@@ -127,7 +133,7 @@ function Screen() {
|
||||
className="overflow-hidden size-full flex-1"
|
||||
>
|
||||
<ScrollArea.Viewport ref={ref} className="relative h-full">
|
||||
<Virtualizer scrollRef={ref}>
|
||||
<Virtualizer scrollRef={ref as unknown as RefObject<HTMLElement>}>
|
||||
{isPending ? (
|
||||
<div className="w-full h-[200px] flex gap-2 items-center justify-center">
|
||||
<Spinner />
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { type ReactNode, memo, useMemo, useRef } from "react";
|
||||
import { type ReactNode, type RefObject, memo, useMemo, useRef } from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
@@ -29,7 +29,10 @@ function Screen() {
|
||||
className="overflow-hidden size-full"
|
||||
>
|
||||
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
|
||||
<Virtualizer scrollRef={ref} overscan={0}>
|
||||
<Virtualizer
|
||||
scrollRef={ref as unknown as RefObject<HTMLElement>}
|
||||
overscan={0}
|
||||
>
|
||||
{!contacts ? (
|
||||
<div className="w-full h-24 flex items-center justify-center">
|
||||
<Spinner className="size-4" />
|
||||
@@ -77,7 +80,7 @@ function StoryItem({ contact }: { contact: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="mb-3 flex flex-col w-full h-[300px] bg-white dark:bg-black rounded-xl border-[.5px] border-neutral-300 dark:border-neutral-700">
|
||||
<div className="mb-3 flex flex-col w-full h-[300px] bg-white dark:bg-neutral-800 rounded-xl border-[.5px] border-neutral-300 dark:border-neutral-700">
|
||||
<div className="h-12 shrink-0 px-2 flex items-center justify-between border-b border-neutral-100 dark:border-white/5">
|
||||
<User.Provider pubkey={contact}>
|
||||
<User.Root className="inline-flex items-center gap-2">
|
||||
@@ -102,7 +105,10 @@ function StoryItem({ contact }: { contact: string }) {
|
||||
className="flex-1 min-h-0 overflow-hidden size-full"
|
||||
>
|
||||
<ScrollArea.Viewport ref={ref} className="relative h-full px-2 pt-2">
|
||||
<Virtualizer scrollRef={ref} overscan={0}>
|
||||
<Virtualizer
|
||||
scrollRef={ref as unknown as RefObject<HTMLElement>}
|
||||
overscan={0}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-full h-[calc(300px-48px)] flex items-center justify-center text-sm">
|
||||
<Spinner className="size-4" />
|
||||
@@ -111,7 +117,7 @@ function StoryItem({ contact }: { contact: string }) {
|
||||
<div className="w-full h-[calc(300px-48px)] flex items-center justify-center text-sm">
|
||||
{error.message}
|
||||
</div>
|
||||
) : !events.length ? (
|
||||
) : !events?.length ? (
|
||||
<div className="w-full h-[calc(300px-48px)] flex items-center justify-center text-sm">
|
||||
This user didn't have any new notes in the last few days.
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Kind, type NostrEvent } from "@/types";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { type RefObject, useCallback, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/trending")({
|
||||
@@ -71,15 +71,18 @@ function Screen() {
|
||||
>
|
||||
<ScrollArea.Viewport
|
||||
ref={ref}
|
||||
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
className="relative h-full bg-white dark:bg-neutral-800 rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<Virtualizer scrollRef={ref} overscan={1}>
|
||||
<Virtualizer
|
||||
scrollRef={ref as unknown as RefObject<HTMLElement>}
|
||||
overscan={1}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center w-full h-16 gap-2">
|
||||
<Spinner className="size-4" />
|
||||
<span className="text-sm font-medium">Loading...</span>
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
) : !data?.length ? (
|
||||
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||
🎉 Yo. You're catching up on all latest notes.
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Kind } from "@/types";
|
||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { type RefObject, useCallback, useRef } from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/columns/_layout/users/$id")({
|
||||
@@ -36,14 +36,17 @@ function Screen() {
|
||||
(event: LumeEvent) => {
|
||||
if (!event) return;
|
||||
switch (event.kind) {
|
||||
case Kind.Repost:
|
||||
case Kind.Repost: {
|
||||
const repostId = event.repostId;
|
||||
|
||||
return (
|
||||
<RepostNote
|
||||
key={event.id}
|
||||
key={repostId + event.id}
|
||||
event={event}
|
||||
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<TextNote
|
||||
@@ -65,9 +68,12 @@ function Screen() {
|
||||
>
|
||||
<ScrollArea.Viewport
|
||||
ref={ref}
|
||||
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
className="relative h-full bg-white dark:bg-neutral-800 rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<Virtualizer scrollRef={ref} overscan={0}>
|
||||
<Virtualizer
|
||||
scrollRef={ref as unknown as RefObject<HTMLElement>}
|
||||
overscan={0}
|
||||
>
|
||||
<User.Provider pubkey={id}>
|
||||
<User.Root className="relative">
|
||||
<User.Cover className="object-cover w-full h-44 rounded-t-lg gradient-mask-b-0" />
|
||||
@@ -88,7 +94,7 @@ function Screen() {
|
||||
<Spinner className="size-5" />
|
||||
<span className="text-sm font-medium">Loading...</span>
|
||||
</div>
|
||||
) : !events.length ? (
|
||||
) : !events?.length ? (
|
||||
<div className="flex items-center justify-center">
|
||||
Yo. You're catching up on all the things happening around you.
|
||||
</div>
|
||||
|
||||
@@ -167,7 +167,7 @@ export class LumeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
static from(raw: string, parsed?: Meta) {
|
||||
static from(raw: string, parsed?: Meta | null) {
|
||||
const nostrEvent: NostrEvent = JSON.parse(raw);
|
||||
|
||||
if (parsed) {
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface NostrEvent {
|
||||
tags: string[][];
|
||||
content: string;
|
||||
sig: string;
|
||||
meta: Meta;
|
||||
meta: Meta | null;
|
||||
}
|
||||
|
||||
export interface EventWithReplies extends NostrEvent {
|
||||
|
||||
Reference in New Issue
Block a user