feat: negentropy progress
This commit is contained in:
@@ -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
26
pnpm-lock.yaml
generated
@@ -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
84
src-tauri/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
wss://relay.damus.io,
|
||||
wss://relay.nostr.net,
|
||||
wss://relay.primal.net,
|
||||
wss://nostr.fmt.wiz.biz,
|
||||
wss://offchain.pub,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }) };
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user