refactor: app settings
This commit is contained in:
@@ -24,8 +24,6 @@
|
|||||||
"@tanstack/query-persist-client-core": "^5.59.16",
|
"@tanstack/query-persist-client-core": "^5.59.16",
|
||||||
"@tanstack/react-query": "^5.59.16",
|
"@tanstack/react-query": "^5.59.16",
|
||||||
"@tanstack/react-router": "^1.77.5",
|
"@tanstack/react-router": "^1.77.5",
|
||||||
"@tanstack/react-store": "^0.5.6",
|
|
||||||
"@tanstack/store": "^0.5.5",
|
|
||||||
"@tauri-apps/api": "^2.0.3",
|
"@tauri-apps/api": "^2.0.3",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -50,12 +50,6 @@ importers:
|
|||||||
'@tanstack/react-router':
|
'@tanstack/react-router':
|
||||||
specifier: ^1.77.5
|
specifier: ^1.77.5
|
||||||
version: 1.77.5(@tanstack/router-generator@1.74.2)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)
|
version: 1.77.5(@tanstack/router-generator@1.74.2)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)
|
||||||
'@tanstack/react-store':
|
|
||||||
specifier: ^0.5.6
|
|
||||||
version: 0.5.6(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)
|
|
||||||
'@tanstack/store':
|
|
||||||
specifier: ^0.5.5
|
|
||||||
version: 0.5.5
|
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
|
|||||||
16
src-tauri/Cargo.lock
generated
16
src-tauri/Cargo.lock
generated
@@ -3480,7 +3480,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -3510,7 +3510,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
@@ -3524,7 +3524,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heed",
|
"heed",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3537,7 +3537,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -3555,7 +3555,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"atomic-destructor",
|
"atomic-destructor",
|
||||||
@@ -3575,7 +3575,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-signer"
|
name = "nostr-signer"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3588,7 +3588,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-zapper"
|
name = "nostr-zapper"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3732,7 +3732,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nwc"
|
name = "nwc"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#282fbc39373674b7394806d47311a8e483da0ef0"
|
source = "git+https://github.com/rust-nostr/nostr#939ebbdbc0b7c605411676e810c775ac3d80ef94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use specta::Type;
|
|||||||
use std::{str::FromStr, time::Duration};
|
use std::{str::FromStr, time::Duration};
|
||||||
use tauri::{Emitter, State};
|
use tauri::{Emitter, State};
|
||||||
|
|
||||||
use crate::{common::get_all_accounts, Nostr};
|
use crate::{common::get_all_accounts, Nostr, Settings};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
struct Account {
|
struct Account {
|
||||||
@@ -249,3 +249,21 @@ pub async fn set_signer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_app_settings(state: State<'_, Nostr>) -> Result<Settings, String> {
|
||||||
|
let settings = state.settings.read().await.clone();
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn set_app_settings(settings: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||||
|
let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?;
|
||||||
|
let mut settings = state.settings.write().await;
|
||||||
|
// Update state
|
||||||
|
settings.clone_from(&parsed);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
|
|||||||
Ok(RichEvent { raw, parsed })
|
Ok(RichEvent { raw, parsed })
|
||||||
} else {
|
} else {
|
||||||
let filter = Filter::new().id(event_id);
|
let filter = Filter::new().id(event_id);
|
||||||
|
|
||||||
let mut rich_event = RichEvent {
|
let mut rich_event = RichEvent {
|
||||||
raw: "".to_string(),
|
raw: "".to_string(),
|
||||||
parsed: None,
|
parsed: None,
|
||||||
|
|||||||
@@ -7,21 +7,9 @@ use tauri::{Emitter, Manager, State};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{get_latest_event, process_event},
|
common::{get_latest_event, process_event},
|
||||||
Nostr, RichEvent, Settings,
|
Nostr, RichEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
|
||||||
pub struct Profile {
|
|
||||||
name: String,
|
|
||||||
display_name: String,
|
|
||||||
about: Option<String>,
|
|
||||||
picture: String,
|
|
||||||
banner: Option<String>,
|
|
||||||
nip05: Option<String>,
|
|
||||||
lud16: Option<String>,
|
|
||||||
website: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||||
pub struct Mention {
|
pub struct Mention {
|
||||||
pubkey: String,
|
pubkey: String,
|
||||||
@@ -116,30 +104,9 @@ pub async fn get_contact_list(id: String, state: State<'_, Nostr>) -> Result<Vec
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn set_profile(profile: Profile, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn set_profile(new_profile: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let mut metadata = Metadata::new()
|
let metadata = Metadata::from_json(new_profile).map_err(|e| e.to_string())?;
|
||||||
.name(profile.name)
|
|
||||||
.display_name(profile.display_name)
|
|
||||||
.about(profile.about.unwrap_or_default())
|
|
||||||
.nip05(profile.nip05.unwrap_or_default())
|
|
||||||
.lud16(profile.lud16.unwrap_or_default());
|
|
||||||
|
|
||||||
if let Ok(url) = Url::parse(&profile.picture) {
|
|
||||||
metadata = metadata.picture(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(b) = profile.banner {
|
|
||||||
if let Ok(url) = Url::parse(&b) {
|
|
||||||
metadata = metadata.banner(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(w) = profile.website {
|
|
||||||
if let Ok(url) = Url::parse(&w) {
|
|
||||||
metadata = metadata.website(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match client.set_metadata(&metadata).await {
|
match client.set_metadata(&metadata).await {
|
||||||
Ok(id) => Ok(id.to_string()),
|
Ok(id) => Ok(id.to_string()),
|
||||||
@@ -612,21 +579,6 @@ pub async fn get_notifications(id: String, state: State<'_, Nostr>) -> Result<Ve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub async fn get_user_settings(state: State<'_, Nostr>) -> Result<Settings, String> {
|
|
||||||
Ok(state.settings.lock().await.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub async fn set_user_settings(settings: String, state: State<'_, Nostr>) -> Result<(), String> {
|
|
||||||
let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?;
|
|
||||||
state.settings.lock().await.clone_from(&parsed);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
|
pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use tauri::{path::BaseDirectory, Emitter, EventTarget, Listener, Manager};
|
|||||||
use tauri_plugin_decorum::WebviewWindowExt;
|
use tauri_plugin_decorum::WebviewWindowExt;
|
||||||
use tauri_plugin_notification::{NotificationExt, PermissionState};
|
use tauri_plugin_notification::{NotificationExt, PermissionState};
|
||||||
use tauri_specta::{collect_commands, Builder};
|
use tauri_specta::{collect_commands, Builder};
|
||||||
use tokio::{sync::Mutex, sync::RwLock, time::sleep};
|
use tokio::{sync::RwLock, time::sleep};
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
@@ -30,7 +30,7 @@ pub mod common;
|
|||||||
pub struct Nostr {
|
pub struct Nostr {
|
||||||
client: Client,
|
client: Client,
|
||||||
queue: RwLock<HashSet<PublicKey>>,
|
queue: RwLock<HashSet<PublicKey>>,
|
||||||
settings: Mutex<Settings>,
|
settings: RwLock<Settings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -40,37 +40,30 @@ pub struct Payload {
|
|||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
proxy: Option<String>,
|
resize_service: bool,
|
||||||
image_resize_service: Option<String>,
|
|
||||||
use_relay_hint: bool,
|
|
||||||
content_warning: bool,
|
content_warning: bool,
|
||||||
trusted_only: bool,
|
|
||||||
display_avatar: bool,
|
display_avatar: bool,
|
||||||
display_zap_button: bool,
|
display_zap_button: bool,
|
||||||
display_repost_button: bool,
|
display_repost_button: bool,
|
||||||
display_media: bool,
|
display_media: bool,
|
||||||
transparent: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
proxy: None,
|
|
||||||
image_resize_service: Some("https://wsrv.nl".to_string()),
|
|
||||||
use_relay_hint: true,
|
|
||||||
content_warning: true,
|
content_warning: true,
|
||||||
trusted_only: false,
|
resize_service: true,
|
||||||
display_avatar: true,
|
display_avatar: true,
|
||||||
display_zap_button: true,
|
display_zap_button: true,
|
||||||
display_repost_button: true,
|
display_repost_button: true,
|
||||||
display_media: true,
|
display_media: true,
|
||||||
transparent: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DEFAULT_DIFFICULTY: u8 = 0;
|
pub const DEFAULT_DIFFICULTY: u8 = 0;
|
||||||
pub const FETCH_LIMIT: usize = 50;
|
pub const FETCH_LIMIT: usize = 50;
|
||||||
|
pub const QUEUE_DELAY: u64 = 300;
|
||||||
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
|
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -113,8 +106,6 @@ fn main() {
|
|||||||
zap_event,
|
zap_event,
|
||||||
copy_friend,
|
copy_friend,
|
||||||
get_notifications,
|
get_notifications,
|
||||||
get_user_settings,
|
|
||||||
set_user_settings,
|
|
||||||
verify_nip05,
|
verify_nip05,
|
||||||
get_meta_from_event,
|
get_meta_from_event,
|
||||||
get_event,
|
get_event,
|
||||||
@@ -138,6 +129,8 @@ fn main() {
|
|||||||
close_column,
|
close_column,
|
||||||
close_all_columns,
|
close_all_columns,
|
||||||
open_window,
|
open_window,
|
||||||
|
get_app_settings,
|
||||||
|
set_app_settings,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -182,7 +175,7 @@ fn main() {
|
|||||||
|
|
||||||
// Config
|
// Config
|
||||||
let opts = Options::new()
|
let opts = Options::new()
|
||||||
.gossip(true)
|
.gossip(false)
|
||||||
.max_avg_latency(Duration::from_millis(300))
|
.max_avg_latency(Duration::from_millis(300))
|
||||||
.automatic_authentication(true)
|
.automatic_authentication(true)
|
||||||
.connection_timeout(Some(Duration::from_secs(5)))
|
.connection_timeout(Some(Duration::from_secs(5)))
|
||||||
@@ -238,7 +231,7 @@ fn main() {
|
|||||||
app.manage(Nostr {
|
app.manage(Nostr {
|
||||||
client,
|
client,
|
||||||
queue: RwLock::new(HashSet::new()),
|
queue: RwLock::new(HashSet::new()),
|
||||||
settings: Mutex::new(Settings::default()),
|
settings: RwLock::new(Settings::default()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for request metadata
|
// Listen for request metadata
|
||||||
@@ -250,22 +243,27 @@ fn main() {
|
|||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let state = handle.state::<Nostr>();
|
let state = handle.state::<Nostr>();
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
let mut write_queue = state.queue.write().await;
|
||||||
|
|
||||||
if let Ok(public_key) = PublicKey::parse(parsed_payload.id) {
|
if let Ok(public_key) = PublicKey::parse(parsed_payload.id) {
|
||||||
let mut write_queue = state.queue.write().await;
|
|
||||||
write_queue.insert(public_key);
|
write_queue.insert(public_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
sleep(Duration::from_millis(300)).await;
|
sleep(Duration::from_millis(QUEUE_DELAY)).await;
|
||||||
|
|
||||||
let read_queue = state.queue.read().await;
|
let read_queue = state.queue.read().await;
|
||||||
|
|
||||||
|
let filter_opts = FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(2));
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().filter(filter_opts);
|
||||||
|
|
||||||
|
let limit = read_queue.len() * 2;
|
||||||
let authors: Vec<PublicKey> = read_queue.iter().copied().collect();
|
let authors: Vec<PublicKey> = read_queue.iter().copied().collect();
|
||||||
let filter = Filter::new().authors(authors).kind(Kind::Metadata);
|
let filter = Filter::new()
|
||||||
let opts = SubscribeAutoCloseOptions::default()
|
.authors(authors)
|
||||||
.filter(FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(3)));
|
.kind(Kind::Metadata)
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
if client.subscribe(vec![filter], Some(opts)).await.is_ok() {
|
if client.subscribe(vec![filter], Some(opts)).await.is_ok() {
|
||||||
let mut write_queue = state.queue.write().await;
|
|
||||||
write_queue.clear();
|
write_queue.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ broadcastQueryClient({
|
|||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
context: { queryClient, platform },
|
context: { store, queryClient, platform },
|
||||||
Wrap: ({ children }) => {
|
Wrap: ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
|||||||
@@ -136,9 +136,9 @@ async getProfile(id: string) : Promise<Result<string, string>> {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setProfile(profile: Profile) : Promise<Result<string, string>> {
|
async setProfile(newProfile: string) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("set_profile", { profile }) };
|
return { status: "ok", data: await TAURI_INVOKE("set_profile", { newProfile }) };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if(e instanceof Error) throw e;
|
if(e instanceof Error) throw e;
|
||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
@@ -288,22 +288,6 @@ async getNotifications(id: string) : Promise<Result<string[], string>> {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getUserSettings() : Promise<Result<Settings, string>> {
|
|
||||||
try {
|
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_user_settings") };
|
|
||||||
} catch (e) {
|
|
||||||
if(e instanceof Error) throw e;
|
|
||||||
else return { status: "error", error: e as any };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async setUserSettings(settings: string) : Promise<Result<null, string>> {
|
|
||||||
try {
|
|
||||||
return { status: "ok", data: await TAURI_INVOKE("set_user_settings", { settings }) };
|
|
||||||
} catch (e) {
|
|
||||||
if(e instanceof Error) throw e;
|
|
||||||
else return { status: "error", error: e as any };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async verifyNip05(id: string, nip05: string) : Promise<Result<boolean, string>> {
|
async verifyNip05(id: string, nip05: string) : Promise<Result<boolean, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("verify_nip05", { id, nip05 }) };
|
return { status: "ok", data: await TAURI_INVOKE("verify_nip05", { id, nip05 }) };
|
||||||
@@ -487,6 +471,22 @@ async openWindow(window: NewWindow) : Promise<Result<string, string>> {
|
|||||||
if(e instanceof Error) throw e;
|
if(e instanceof Error) throw e;
|
||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async getAppSettings() : Promise<Result<Settings, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_app_settings") };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setAppSettings(settings: string) : Promise<Result<null, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("set_app_settings", { settings }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,10 +504,9 @@ export type Column = { label: string; url: string; x: number; y: number; width:
|
|||||||
export type Mention = { pubkey: string; avatar: string; display_name: string; name: string }
|
export type Mention = { pubkey: string; avatar: string; display_name: string; name: string }
|
||||||
export type Meta = { content: string; images: string[]; events: string[]; mentions: string[]; hashtags: string[] }
|
export type Meta = { content: string; images: string[]; events: string[]; mentions: string[]; hashtags: string[] }
|
||||||
export type NewWindow = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean; closable: boolean }
|
export type NewWindow = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean; closable: boolean }
|
||||||
export type Profile = { name: string; display_name: string; about: string | null; picture: string; banner: string | null; nip05: string | null; lud16: string | null; website: string | null }
|
|
||||||
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
|
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
|
||||||
export type RichEvent = { raw: string; parsed: Meta | null }
|
export type RichEvent = { raw: string; parsed: Meta | null }
|
||||||
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; trusted_only: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; transparent: boolean }
|
export type Settings = { resize_service: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean }
|
||||||
export type TAURI_CHANNEL<TSend> = null
|
export type TAURI_CHANNEL<TSend> = null
|
||||||
|
|
||||||
/** tauri-specta globals **/
|
/** tauri-specta globals **/
|
||||||
|
|||||||
136
src/commons.ts
136
src/commons.ts
@@ -3,7 +3,6 @@ import type {
|
|||||||
MaybePromise,
|
MaybePromise,
|
||||||
PersistedQuery,
|
PersistedQuery,
|
||||||
} from "@tanstack/query-persist-client-core";
|
} from "@tanstack/query-persist-client-core";
|
||||||
import { Store } from "@tanstack/react-store";
|
|
||||||
import { ask, message, open } from "@tauri-apps/plugin-dialog";
|
import { ask, message, open } from "@tauri-apps/plugin-dialog";
|
||||||
import { readFile } from "@tauri-apps/plugin-fs";
|
import { readFile } from "@tauri-apps/plugin-fs";
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
@@ -16,76 +15,79 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
|||||||
import updateLocale from "dayjs/plugin/updateLocale";
|
import updateLocale from "dayjs/plugin/updateLocale";
|
||||||
import { decode } from "light-bolt11-decoder";
|
import { decode } from "light-bolt11-decoder";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import type { RichEvent, Settings } from "./commands.gen";
|
import type { RichEvent } from "./commands.gen";
|
||||||
import { LumeEvent } from "./system";
|
import { LumeEvent } from "./system";
|
||||||
import type { LumeColumn, NostrEvent } from "./types";
|
import type { NostrEvent } from "./types";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
dayjs.extend(updateLocale);
|
|
||||||
|
|
||||||
dayjs.updateLocale("en", {
|
|
||||||
relativeTime: {
|
|
||||||
past: "%s ago",
|
|
||||||
s: "just now",
|
|
||||||
m: "1m",
|
|
||||||
mm: "%dm",
|
|
||||||
h: "1h",
|
|
||||||
hh: "%dh",
|
|
||||||
d: "1d",
|
|
||||||
dd: "%dd",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isImagePath = (path: string) => {
|
export const isImagePath = (path: string) => {
|
||||||
for (const suffix of ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"]) {
|
const exts = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
||||||
if (path.endsWith(suffix)) return true;
|
|
||||||
|
for (const suffix of exts) {
|
||||||
|
if (path.endsWith(suffix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isImageUrl = (url: string) => {
|
export function createdAt(time: number) {
|
||||||
try {
|
// Config for dayjs
|
||||||
if (!url) return false;
|
dayjs.extend(relativeTime);
|
||||||
const ext = new URL(url).pathname.split(".").pop();
|
dayjs.extend(updateLocale);
|
||||||
return ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"].includes(ext);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function formatCreatedAt(time: number, message = false) {
|
// Config locale text
|
||||||
let formated: string;
|
dayjs.updateLocale("en", {
|
||||||
|
relativeTime: {
|
||||||
|
past: "%s ago",
|
||||||
|
s: "just now",
|
||||||
|
m: "1m",
|
||||||
|
mm: "%dm",
|
||||||
|
h: "1h",
|
||||||
|
hh: "%dh",
|
||||||
|
d: "1d",
|
||||||
|
dd: "%dd",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
const inputTime = dayjs.unix(time);
|
const inputTime = dayjs.unix(time);
|
||||||
const diff = now.diff(inputTime, "hour");
|
const diff = now.diff(inputTime, "hour");
|
||||||
|
|
||||||
if (message) {
|
if (diff < 24) {
|
||||||
if (diff < 12) {
|
return inputTime.from(now, true);
|
||||||
formated = inputTime.format("HH:mm A");
|
|
||||||
} else {
|
|
||||||
formated = inputTime.format("MMM DD");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (diff < 24) {
|
return inputTime.format("MMM DD");
|
||||||
formated = inputTime.from(now, true);
|
|
||||||
} else {
|
|
||||||
formated = inputTime.format("MMM DD");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return formated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replyTime(time: number) {
|
export function replyAt(time: number) {
|
||||||
const inputTime = dayjs.unix(time);
|
// Config for dayjs
|
||||||
const formated = inputTime.format("MM-DD-YY HH:mm");
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.extend(updateLocale);
|
||||||
|
|
||||||
return formated;
|
// Config locale text
|
||||||
|
dayjs.updateLocale("en", {
|
||||||
|
relativeTime: {
|
||||||
|
past: "%s ago",
|
||||||
|
s: "just now",
|
||||||
|
m: "1m",
|
||||||
|
mm: "%dm",
|
||||||
|
h: "1h",
|
||||||
|
hh: "%dh",
|
||||||
|
d: "1d",
|
||||||
|
dd: "%dd",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputTime = dayjs.unix(time);
|
||||||
|
const format = inputTime.format("MM-DD-YY HH:mm");
|
||||||
|
|
||||||
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displayNpub(pubkey: string, len: number) {
|
export function displayNpub(pubkey: string, len: number) {
|
||||||
@@ -114,7 +116,7 @@ export function displayLongHandle(str: string) {
|
|||||||
return `${handle.substring(0, 16)}...@${service}`;
|
return `${handle.substring(0, 16)}...@${service}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// source: https://github.com/synonymdev/bitkit/blob/master/src/utils/displayValues/index.ts
|
// Source: https://github.com/synonymdev/bitkit/blob/master/src/utils/displayValues/index.ts
|
||||||
export function getBitcoinDisplayValues(satoshis: number) {
|
export function getBitcoinDisplayValues(satoshis: number) {
|
||||||
let bitcoinFormatted = new BitcoinUnit(satoshis, "satoshis")
|
let bitcoinFormatted = new BitcoinUnit(satoshis, "satoshis")
|
||||||
.getValue()
|
.getValue()
|
||||||
@@ -145,20 +147,27 @@ export function getBitcoinDisplayValues(satoshis: number) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeZapInvoice(tags?: string[][]) {
|
export function decodeZapInvoice(tags: string[][]) {
|
||||||
const invoice = tags.find((tag) => tag[0] === "bolt11")?.[1];
|
const invoice = tags.find((tag) => tag[0] === "bolt11")?.[1];
|
||||||
if (!invoice) return;
|
if (!invoice) return;
|
||||||
|
|
||||||
const decodedInvoice = decode(invoice);
|
const decodedInvoice = decode(invoice);
|
||||||
const amountSection = decodedInvoice.sections.find(
|
const section = decodedInvoice.sections.find(
|
||||||
(s: { name: string }) => s.name === "amount",
|
(s: { name: string }) => s.name === "amount",
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore, its fine.
|
if (!section) {
|
||||||
const amount = Number.parseInt(amountSection.value) / 1000;
|
return null;
|
||||||
const displayValue = getBitcoinDisplayValues(amount);
|
}
|
||||||
|
|
||||||
return displayValue;
|
if (section.name === "amount") {
|
||||||
|
const amount = Number.parseInt(section.value) / 1000;
|
||||||
|
const displayValue = getBitcoinDisplayValues(amount);
|
||||||
|
|
||||||
|
return displayValue;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkForAppUpdates(silent: boolean) {
|
export async function checkForAppUpdates(silent: boolean) {
|
||||||
@@ -277,18 +286,3 @@ export function newQueryStorage(
|
|||||||
(await store.delete(key)) as unknown as MaybePromise<void>,
|
(await store.delete(key)) as unknown as MaybePromise<void>,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appSettings = new Store<Settings>({
|
|
||||||
proxy: null,
|
|
||||||
image_resize_service: "https://wsrv.nl",
|
|
||||||
use_relay_hint: true,
|
|
||||||
content_warning: true,
|
|
||||||
trusted_only: true,
|
|
||||||
display_avatar: true,
|
|
||||||
display_zap_button: true,
|
|
||||||
display_repost_button: true,
|
|
||||||
display_media: true,
|
|
||||||
transparent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const appColumns = new Store<LumeColumn[]>([]);
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { appSettings, cn, displayNpub } from "@/commons";
|
import { cn, displayNpub } from "@/commons";
|
||||||
import { RepostIcon, Spinner } from "@/components";
|
import { RepostIcon, Spinner } from "@/components";
|
||||||
|
import { settingsQueryOptions } from "@/routes/__root";
|
||||||
import type { Metadata } from "@/types";
|
import type { Metadata } from "@/types";
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import {
|
||||||
import { useStore } from "@tanstack/react-store";
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
useSuspenseQuery,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useCallback, useTransition } from "react";
|
import { useCallback, useTransition } from "react";
|
||||||
@@ -15,7 +20,7 @@ export function NoteRepost({
|
|||||||
smol = false,
|
smol = false,
|
||||||
}: { label?: boolean; smol?: boolean }) {
|
}: { label?: boolean; smol?: boolean }) {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
const visible = useStore(appSettings, (state) => state.display_repost_button);
|
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { isLoading, data: status } = useQuery({
|
const { isLoading, data: status } = useQuery({
|
||||||
@@ -28,7 +33,7 @@ export function NoteRepost({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled: visible,
|
enabled: settings.data.display_repost_button,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
@@ -120,7 +125,7 @@ export function NoteRepost({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!visible) return null;
|
if (!settings.data.display_repost_button) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip.Provider>
|
<Tooltip.Provider>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { appSettings, cn } from "@/commons";
|
import { cn } from "@/commons";
|
||||||
import { ZapIcon } from "@/components";
|
import { ZapIcon } from "@/components";
|
||||||
|
import { settingsQueryOptions } from "@/routes/__root";
|
||||||
import { LumeWindow } from "@/system";
|
import { LumeWindow } from "@/system";
|
||||||
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { useStore } from "@tanstack/react-store";
|
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
|
|
||||||
export function NoteZap({
|
export function NoteZap({
|
||||||
@@ -10,10 +11,10 @@ export function NoteZap({
|
|||||||
smol = false,
|
smol = false,
|
||||||
}: { label?: boolean; smol?: boolean }) {
|
}: { label?: boolean; smol?: boolean }) {
|
||||||
const search = useSearch({ strict: false });
|
const search = useSearch({ strict: false });
|
||||||
const visible = useStore(appSettings, (state) => state.display_zap_button);
|
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
|
|
||||||
if (!visible) return null;
|
if (!settings.data.display_zap_button) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { appSettings, cn } from "@/commons";
|
import { cn } from "@/commons";
|
||||||
import { useStore } from "@tanstack/react-store";
|
import { settingsQueryOptions } from "@/routes/__root";
|
||||||
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { type ReactNode, useMemo, useState } from "react";
|
import { type ReactNode, useMemo, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
@@ -21,14 +22,17 @@ export function NoteContent({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
const visible = useStore(appSettings, (state) => state.display_media);
|
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||||
|
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
// Get parsed meta
|
// Get parsed meta
|
||||||
const { content, hashtags, events, mentions } = event.meta;
|
const { content, hashtags, events, mentions } = event.meta;
|
||||||
|
|
||||||
// Define rich content
|
// Define rich content
|
||||||
let richContent: ReactNode[] | string = visible ? content : event.content;
|
let richContent: ReactNode[] | string = settings.data.display_media
|
||||||
|
? content
|
||||||
|
: event.content;
|
||||||
|
|
||||||
for (const hashtag of hashtags) {
|
for (const hashtag of hashtags) {
|
||||||
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
|
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
|
||||||
@@ -103,7 +107,7 @@ export function NoteContent({
|
|||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
{visible ? (
|
{settings.data.display_media ? (
|
||||||
event.meta?.images.length ? (
|
event.meta?.images.length ? (
|
||||||
<Images urls={event.meta.images} />
|
<Images urls={event.meta.images} />
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { replyTime } from "@/commons";
|
import { replyAt } from "@/commons";
|
||||||
import { Note, Spinner } from "@/components";
|
import { Note, Spinner, User } from "@/components";
|
||||||
import { User } from "@/components/user";
|
|
||||||
import { LumeWindow, useEvent } from "@/system";
|
import { LumeWindow, useEvent } from "@/system";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { type ReactNode, memo, useMemo } from "react";
|
import { type ReactNode, memo, useMemo } from "react";
|
||||||
@@ -52,7 +51,7 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-between">
|
<div className="flex-1 flex items-center justify-between">
|
||||||
<span className="text-sm text-neutral-500">
|
<span className="text-sm text-neutral-500">
|
||||||
{replyTime(event.created_at)}
|
{replyAt(event.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<div className="invisible group-hover:visible flex items-center justify-end">
|
<div className="invisible group-hover:visible flex items-center justify-end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import { appSettings } from "@/commons";
|
import { settingsQueryOptions } from "@/routes/__root";
|
||||||
import { useStore } from "@tanstack/react-store";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function ImagePreview({ url }: { url: string }) {
|
export function ImagePreview({ url }: { url: string }) {
|
||||||
const [service, visible] = useStore(appSettings, (state) => [
|
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||||
state.image_resize_service,
|
|
||||||
state.display_media,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const imageUrl = useMemo(() => {
|
const imageUrl = useMemo(() => {
|
||||||
if (service?.length) {
|
if (settings.data.resize_service) {
|
||||||
const newUrl = `${service}?url=${url}&ll&af&default=1&n=-1`;
|
const newUrl = `https://wsrv.nl?url=${url}&ll&af&default=1&n=-1`;
|
||||||
return newUrl;
|
return newUrl;
|
||||||
} else {
|
} else {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}, [service]);
|
}, [settings.data.resize_service]);
|
||||||
|
|
||||||
if (!visible) {
|
if (!settings.data.display_media) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={url}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import { appSettings, cn } from "@/commons";
|
import { cn } from "@/commons";
|
||||||
import { Spinner } from "@/components";
|
import { Spinner } from "@/components";
|
||||||
|
import { settingsQueryOptions } from "@/routes/__root";
|
||||||
import { ArrowLeft, ArrowRight } from "@phosphor-icons/react";
|
import { ArrowLeft, ArrowRight } from "@phosphor-icons/react";
|
||||||
import { useStore } from "@tanstack/react-store";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { open } from "@tauri-apps/plugin-shell";
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
import useEmblaCarousel from "embla-carousel-react";
|
import useEmblaCarousel from "embla-carousel-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
export function Images({ urls }: { urls: string[] }) {
|
export function Images({ urls }: { urls: string[] }) {
|
||||||
const [slidesInView, setSlidesInView] = useState([]);
|
const [slidesInView, setSlidesInView] = useState<number[]>([]);
|
||||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||||
dragFree: true,
|
dragFree: true,
|
||||||
align: "start",
|
align: "start",
|
||||||
watchSlides: false,
|
watchSlides: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const service = useStore(appSettings, (state) => state.image_resize_service);
|
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||||
|
|
||||||
const imageUrls = useMemo(() => {
|
const imageUrls = useMemo(() => {
|
||||||
if (service?.length) {
|
if (settings.data.resize_service) {
|
||||||
let newUrls: string[];
|
let newUrls: string[];
|
||||||
|
|
||||||
if (urls.length === 1) {
|
if (urls.length === 1) {
|
||||||
@@ -28,11 +29,12 @@ export function Images({ urls }: { urls: string[] }) {
|
|||||||
if (url.includes("bsky.network")) {
|
if (url.includes("bsky.network")) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
return `${service}?url=${url}&ll&af&default=1&n=-1`;
|
return `https://wsrv.nl?url=${url}&ll&af&default=1&n=-1`;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
newUrls = urls.map(
|
newUrls = urls.map(
|
||||||
(url) => `${service}?url=${url}&w=480&h=640&ll&af&default=1&n=-1`,
|
(url) =>
|
||||||
|
`https://wsrv.nl?url=${url}&w=480&h=640&ll&af&default=1&n=-1`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ export function Images({ urls }: { urls: string[] }) {
|
|||||||
} else {
|
} else {
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
}, [service]);
|
}, [settings.data.resize_service]);
|
||||||
|
|
||||||
const scrollPrev = useCallback(() => {
|
const scrollPrev = useCallback(() => {
|
||||||
if (emblaApi) emblaApi.scrollPrev();
|
if (emblaApi) emblaApi.scrollPrev();
|
||||||
@@ -50,23 +52,27 @@ export function Images({ urls }: { urls: string[] }) {
|
|||||||
if (emblaApi) emblaApi.scrollNext();
|
if (emblaApi) emblaApi.scrollNext();
|
||||||
}, [emblaApi]);
|
}, [emblaApi]);
|
||||||
|
|
||||||
const updateSlidesInView = useCallback((emblaApi) => {
|
const updateSlidesInView = useCallback(() => {
|
||||||
setSlidesInView((slidesInView) => {
|
setSlidesInView((slidesInView) => {
|
||||||
if (slidesInView.length === emblaApi.slideNodes().length) {
|
if (slidesInView.length === emblaApi?.slideNodes().length) {
|
||||||
emblaApi.off("slidesInView", updateSlidesInView);
|
emblaApi?.off("slidesInView", updateSlidesInView);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inView = emblaApi
|
const inView = emblaApi
|
||||||
.slidesInView()
|
?.slidesInView()
|
||||||
.filter((index) => !slidesInView.includes(index));
|
.filter((index) => !slidesInView.includes(index));
|
||||||
|
|
||||||
return slidesInView.concat(inView);
|
if (inView) {
|
||||||
|
return slidesInView.concat(inView);
|
||||||
|
} else {
|
||||||
|
return slidesInView;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, [emblaApi]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (emblaApi && urls.length > 1) {
|
if (emblaApi && urls.length > 1) {
|
||||||
updateSlidesInView(emblaApi);
|
updateSlidesInView();
|
||||||
|
|
||||||
emblaApi.on("slidesInView", updateSlidesInView);
|
emblaApi.on("slidesInView", updateSlidesInView);
|
||||||
emblaApi.on("reInit", updateSlidesInView);
|
emblaApi.on("reInit", updateSlidesInView);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { appSettings } from "@/commons";
|
import { settingsQueryOptions } from "@/routes/__root";
|
||||||
import { useStore } from "@tanstack/react-store";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export function VideoPreview({ url }: { url: string }) {
|
export function VideoPreview({ url }: { url: string }) {
|
||||||
const visible = useStore(appSettings, (state) => state.display_media);
|
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||||
|
|
||||||
if (!visible) {
|
if (!settings.data.display_zap_button) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={url}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { cn, replyTime } from "@/commons";
|
import { cn, replyAt } from "@/commons";
|
||||||
import { Note } from "@/components/note";
|
import { Note, User } from "@/components";
|
||||||
import { type LumeEvent, LumeWindow } from "@/system";
|
import { type LumeEvent, LumeWindow } from "@/system";
|
||||||
import { CaretDown } from "@phosphor-icons/react";
|
import { CaretDown } from "@phosphor-icons/react";
|
||||||
import { Link, useSearch } from "@tanstack/react-router";
|
import { Link, useSearch } from "@tanstack/react-router";
|
||||||
@@ -10,7 +10,6 @@ import { type ReactNode, memo, useCallback, useMemo } from "react";
|
|||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import { Hashtag } from "./note/mentions/hashtag";
|
import { Hashtag } from "./note/mentions/hashtag";
|
||||||
import { MentionUser } from "./note/mentions/user";
|
import { MentionUser } from "./note/mentions/user";
|
||||||
import { User } from "./user";
|
|
||||||
|
|
||||||
export const ReplyNote = memo(function ReplyNote({
|
export const ReplyNote = memo(function ReplyNote({
|
||||||
event,
|
event,
|
||||||
@@ -66,7 +65,7 @@ export const ReplyNote = memo(function ReplyNote({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-between">
|
<div className="flex-1 flex items-center justify-between">
|
||||||
<span className="text-sm text-neutral-500">
|
<span className="text-sm text-neutral-500">
|
||||||
{replyTime(event.created_at)}
|
{replyAt(event.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<Note.Reply smol />
|
<Note.Reply smol />
|
||||||
@@ -147,7 +146,7 @@ function ChildReply({ event }: { event: LumeEvent }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-between">
|
<div className="flex-1 flex items-center justify-between">
|
||||||
<span className="text-sm text-neutral-500">
|
<span className="text-sm text-neutral-500">
|
||||||
{replyTime(event.created_at)}
|
{replyAt(event.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<div className="invisible group-hover:visible flex items-center justify-end">
|
<div className="invisible group-hover:visible flex items-center justify-end">
|
||||||
<Note.Reply smol />
|
<Note.Reply smol />
|
||||||
|
|||||||
@@ -1,38 +1,36 @@
|
|||||||
import { appSettings, cn } from "@/commons";
|
import { cn } from "@/commons";
|
||||||
|
import { settingsQueryOptions } from "@/routes/__root";
|
||||||
import * as Avatar from "@radix-ui/react-avatar";
|
import * as Avatar from "@radix-ui/react-avatar";
|
||||||
import { useStore } from "@tanstack/react-store";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { minidenticon } from "minidenticons";
|
import { minidenticon } from "minidenticons";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useUserContext } from "./provider";
|
import { useUserContext } from "./provider";
|
||||||
|
|
||||||
export function UserAvatar({ className }: { className?: string }) {
|
export function UserAvatar({ className }: { className?: string }) {
|
||||||
const [service, visible] = useStore(appSettings, (state) => [
|
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||||
state.image_resize_service,
|
|
||||||
state.display_avatar,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
|
|
||||||
const picture = useMemo(() => {
|
const picture = useMemo(() => {
|
||||||
if (service?.length && user.profile?.picture?.length) {
|
if (settings.data.resize_service && user?.profile?.picture?.length) {
|
||||||
if (user.profile?.picture.includes("_next/")) {
|
if (user.profile?.picture.includes("_next/")) {
|
||||||
return user.profile?.picture;
|
return user.profile?.picture;
|
||||||
}
|
}
|
||||||
if (user.profile?.picture.includes("bsky.network")) {
|
if (user.profile?.picture.includes("bsky.network")) {
|
||||||
return user.profile?.picture;
|
return user.profile?.picture;
|
||||||
}
|
}
|
||||||
return `${service}?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${user.profile?.picture}`;
|
return `https://wsrv.nl?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${user.profile?.picture}`;
|
||||||
} else {
|
} else {
|
||||||
return user.profile?.picture;
|
return user?.profile?.picture;
|
||||||
}
|
}
|
||||||
}, [user.profile?.picture]);
|
}, [user]);
|
||||||
|
|
||||||
const fallback = useMemo(
|
const fallback = useMemo(
|
||||||
() =>
|
() =>
|
||||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||||
minidenticon(user.pubkey, 60, 50),
|
minidenticon(user ? user.pubkey : "lume", 60, 50),
|
||||||
)}`,
|
)}`,
|
||||||
[user.pubkey],
|
[user],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,18 +40,19 @@ export function UserAvatar({ className }: { className?: string }) {
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{visible ? (
|
{settings.data.display_avatar ? (
|
||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
src={picture}
|
src={picture}
|
||||||
alt={user.pubkey}
|
alt={user?.pubkey}
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
className="w-full aspect-square object-cover outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Avatar.Fallback>
|
<Avatar.Fallback>
|
||||||
<img
|
<img
|
||||||
src={fallback}
|
src={fallback}
|
||||||
alt={user.pubkey}
|
alt={user?.pubkey}
|
||||||
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
className="size-full bg-black dark:bg-white outline-[.5px] outline-black/5 content-visibility-auto contain-intrinsic-size-[auto]"
|
||||||
/>
|
/>
|
||||||
</Avatar.Fallback>
|
</Avatar.Fallback>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn, formatCreatedAt } from "@/commons";
|
import { cn, createdAt } from "@/commons";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function UserTime({
|
export function UserTime({
|
||||||
@@ -8,11 +8,11 @@ export function UserTime({
|
|||||||
time: number;
|
time: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const createdAt = useMemo(() => formatCreatedAt(time), [time]);
|
const displayCreatedAt = useMemo(() => createdAt(time), [time]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("text-neutral-600 dark:text-neutral-400", className)}>
|
<div className={cn("text-neutral-600 dark:text-neutral-400", className)}>
|
||||||
{createdAt}
|
{displayCreatedAt}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ import { Route as NewPostIndexImport } from './routes/new-post/index'
|
|||||||
import { Route as AppIndexImport } from './routes/_app/index'
|
import { Route as AppIndexImport } from './routes/_app/index'
|
||||||
import { Route as ZapIdImport } from './routes/zap.$id'
|
import { Route as ZapIdImport } from './routes/zap.$id'
|
||||||
import { Route as ColumnsLayoutImport } from './routes/columns/_layout'
|
import { Route as ColumnsLayoutImport } from './routes/columns/_layout'
|
||||||
|
import { Route as IdSetProfileImport } from './routes/$id.set-profile'
|
||||||
import { Route as IdSetInterestImport } from './routes/$id.set-interest'
|
import { Route as IdSetInterestImport } from './routes/$id.set-interest'
|
||||||
import { Route as IdSetGroupImport } from './routes/$id.set-group'
|
import { Route as IdSetGroupImport } from './routes/$id.set-group'
|
||||||
import { Route as SettingsIdWalletImport } from './routes/settings.$id/wallet'
|
import { Route as SettingsIdWalletImport } from './routes/settings.$id/wallet'
|
||||||
import { Route as SettingsIdRelayImport } from './routes/settings.$id/relay'
|
import { Route as SettingsIdRelayImport } from './routes/settings.$id/relay'
|
||||||
import { Route as SettingsIdProfileImport } from './routes/settings.$id/profile'
|
|
||||||
import { Route as SettingsIdGeneralImport } from './routes/settings.$id/general'
|
import { Route as SettingsIdGeneralImport } from './routes/settings.$id/general'
|
||||||
import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global'
|
import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global'
|
||||||
import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed'
|
import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed'
|
||||||
@@ -149,6 +149,14 @@ const ColumnsLayoutRoute = ColumnsLayoutImport.update({
|
|||||||
getParentRoute: () => ColumnsRoute,
|
getParentRoute: () => ColumnsRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const IdSetProfileRoute = IdSetProfileImport.update({
|
||||||
|
id: '/$id/set-profile',
|
||||||
|
path: '/$id/set-profile',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() =>
|
||||||
|
import('./routes/$id.set-profile.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const IdSetInterestRoute = IdSetInterestImport.update({
|
const IdSetInterestRoute = IdSetInterestImport.update({
|
||||||
id: '/$id/set-interest',
|
id: '/$id/set-interest',
|
||||||
path: '/$id/set-interest',
|
path: '/$id/set-interest',
|
||||||
@@ -204,14 +212,6 @@ const SettingsIdRelayRoute = SettingsIdRelayImport.update({
|
|||||||
import('./routes/settings.$id/relay.lazy').then((d) => d.Route),
|
import('./routes/settings.$id/relay.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
const SettingsIdProfileRoute = SettingsIdProfileImport.update({
|
|
||||||
id: '/profile',
|
|
||||||
path: '/profile',
|
|
||||||
getParentRoute: () => SettingsIdLazyRoute,
|
|
||||||
} as any).lazy(() =>
|
|
||||||
import('./routes/settings.$id/profile.lazy').then((d) => d.Route),
|
|
||||||
)
|
|
||||||
|
|
||||||
const SettingsIdGeneralRoute = SettingsIdGeneralImport.update({
|
const SettingsIdGeneralRoute = SettingsIdGeneralImport.update({
|
||||||
id: '/general',
|
id: '/general',
|
||||||
path: '/general',
|
path: '/general',
|
||||||
@@ -364,6 +364,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IdSetInterestImport
|
preLoaderRoute: typeof IdSetInterestImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/$id/set-profile': {
|
||||||
|
id: '/$id/set-profile'
|
||||||
|
path: '/$id/set-profile'
|
||||||
|
fullPath: '/$id/set-profile'
|
||||||
|
preLoaderRoute: typeof IdSetProfileImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/columns': {
|
'/columns': {
|
||||||
id: '/columns'
|
id: '/columns'
|
||||||
path: '/columns'
|
path: '/columns'
|
||||||
@@ -448,13 +455,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof SettingsIdGeneralImport
|
preLoaderRoute: typeof SettingsIdGeneralImport
|
||||||
parentRoute: typeof SettingsIdLazyImport
|
parentRoute: typeof SettingsIdLazyImport
|
||||||
}
|
}
|
||||||
'/settings/$id/profile': {
|
|
||||||
id: '/settings/$id/profile'
|
|
||||||
path: '/profile'
|
|
||||||
fullPath: '/settings/$id/profile'
|
|
||||||
preLoaderRoute: typeof SettingsIdProfileImport
|
|
||||||
parentRoute: typeof SettingsIdLazyImport
|
|
||||||
}
|
|
||||||
'/settings/$id/relay': {
|
'/settings/$id/relay': {
|
||||||
id: '/settings/$id/relay'
|
id: '/settings/$id/relay'
|
||||||
path: '/relay'
|
path: '/relay'
|
||||||
@@ -651,14 +651,12 @@ const ColumnsRouteWithChildren =
|
|||||||
|
|
||||||
interface SettingsIdLazyRouteChildren {
|
interface SettingsIdLazyRouteChildren {
|
||||||
SettingsIdGeneralRoute: typeof SettingsIdGeneralRoute
|
SettingsIdGeneralRoute: typeof SettingsIdGeneralRoute
|
||||||
SettingsIdProfileRoute: typeof SettingsIdProfileRoute
|
|
||||||
SettingsIdRelayRoute: typeof SettingsIdRelayRoute
|
SettingsIdRelayRoute: typeof SettingsIdRelayRoute
|
||||||
SettingsIdWalletRoute: typeof SettingsIdWalletRoute
|
SettingsIdWalletRoute: typeof SettingsIdWalletRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsIdLazyRouteChildren: SettingsIdLazyRouteChildren = {
|
const SettingsIdLazyRouteChildren: SettingsIdLazyRouteChildren = {
|
||||||
SettingsIdGeneralRoute: SettingsIdGeneralRoute,
|
SettingsIdGeneralRoute: SettingsIdGeneralRoute,
|
||||||
SettingsIdProfileRoute: SettingsIdProfileRoute,
|
|
||||||
SettingsIdRelayRoute: SettingsIdRelayRoute,
|
SettingsIdRelayRoute: SettingsIdRelayRoute,
|
||||||
SettingsIdWalletRoute: SettingsIdWalletRoute,
|
SettingsIdWalletRoute: SettingsIdWalletRoute,
|
||||||
}
|
}
|
||||||
@@ -673,6 +671,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/new': typeof NewLazyRoute
|
'/new': typeof NewLazyRoute
|
||||||
'/$id/set-group': typeof IdSetGroupRoute
|
'/$id/set-group': typeof IdSetGroupRoute
|
||||||
'/$id/set-interest': typeof IdSetInterestRoute
|
'/$id/set-interest': typeof IdSetInterestRoute
|
||||||
|
'/$id/set-profile': typeof IdSetProfileRoute
|
||||||
'/columns': typeof ColumnsLayoutRouteWithChildren
|
'/columns': typeof ColumnsLayoutRouteWithChildren
|
||||||
'/zap/$id': typeof ZapIdRoute
|
'/zap/$id': typeof ZapIdRoute
|
||||||
'/new-account/connect': typeof NewAccountConnectLazyRoute
|
'/new-account/connect': typeof NewAccountConnectLazyRoute
|
||||||
@@ -684,7 +683,6 @@ export interface FileRoutesByFullPath {
|
|||||||
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||||
'/settings/$id/general': typeof SettingsIdGeneralRoute
|
'/settings/$id/general': typeof SettingsIdGeneralRoute
|
||||||
'/settings/$id/profile': typeof SettingsIdProfileRoute
|
|
||||||
'/settings/$id/relay': typeof SettingsIdRelayRoute
|
'/settings/$id/relay': typeof SettingsIdRelayRoute
|
||||||
'/settings/$id/wallet': typeof SettingsIdWalletRoute
|
'/settings/$id/wallet': typeof SettingsIdWalletRoute
|
||||||
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
@@ -708,6 +706,7 @@ export interface FileRoutesByTo {
|
|||||||
'/new': typeof NewLazyRoute
|
'/new': typeof NewLazyRoute
|
||||||
'/$id/set-group': typeof IdSetGroupRoute
|
'/$id/set-group': typeof IdSetGroupRoute
|
||||||
'/$id/set-interest': typeof IdSetInterestRoute
|
'/$id/set-interest': typeof IdSetInterestRoute
|
||||||
|
'/$id/set-profile': typeof IdSetProfileRoute
|
||||||
'/columns': typeof ColumnsLayoutRouteWithChildren
|
'/columns': typeof ColumnsLayoutRouteWithChildren
|
||||||
'/zap/$id': typeof ZapIdRoute
|
'/zap/$id': typeof ZapIdRoute
|
||||||
'/new-account/connect': typeof NewAccountConnectLazyRoute
|
'/new-account/connect': typeof NewAccountConnectLazyRoute
|
||||||
@@ -719,7 +718,6 @@ export interface FileRoutesByTo {
|
|||||||
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||||
'/settings/$id/general': typeof SettingsIdGeneralRoute
|
'/settings/$id/general': typeof SettingsIdGeneralRoute
|
||||||
'/settings/$id/profile': typeof SettingsIdProfileRoute
|
|
||||||
'/settings/$id/relay': typeof SettingsIdRelayRoute
|
'/settings/$id/relay': typeof SettingsIdRelayRoute
|
||||||
'/settings/$id/wallet': typeof SettingsIdWalletRoute
|
'/settings/$id/wallet': typeof SettingsIdWalletRoute
|
||||||
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
@@ -745,6 +743,7 @@ export interface FileRoutesById {
|
|||||||
'/new': typeof NewLazyRoute
|
'/new': typeof NewLazyRoute
|
||||||
'/$id/set-group': typeof IdSetGroupRoute
|
'/$id/set-group': typeof IdSetGroupRoute
|
||||||
'/$id/set-interest': typeof IdSetInterestRoute
|
'/$id/set-interest': typeof IdSetInterestRoute
|
||||||
|
'/$id/set-profile': typeof IdSetProfileRoute
|
||||||
'/columns': typeof ColumnsRouteWithChildren
|
'/columns': typeof ColumnsRouteWithChildren
|
||||||
'/columns/_layout': typeof ColumnsLayoutRouteWithChildren
|
'/columns/_layout': typeof ColumnsLayoutRouteWithChildren
|
||||||
'/zap/$id': typeof ZapIdRoute
|
'/zap/$id': typeof ZapIdRoute
|
||||||
@@ -757,7 +756,6 @@ export interface FileRoutesById {
|
|||||||
'/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
'/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
|
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
|
||||||
'/settings/$id/general': typeof SettingsIdGeneralRoute
|
'/settings/$id/general': typeof SettingsIdGeneralRoute
|
||||||
'/settings/$id/profile': typeof SettingsIdProfileRoute
|
|
||||||
'/settings/$id/relay': typeof SettingsIdRelayRoute
|
'/settings/$id/relay': typeof SettingsIdRelayRoute
|
||||||
'/settings/$id/wallet': typeof SettingsIdWalletRoute
|
'/settings/$id/wallet': typeof SettingsIdWalletRoute
|
||||||
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
@@ -784,6 +782,7 @@ export interface FileRouteTypes {
|
|||||||
| '/new'
|
| '/new'
|
||||||
| '/$id/set-group'
|
| '/$id/set-group'
|
||||||
| '/$id/set-interest'
|
| '/$id/set-interest'
|
||||||
|
| '/$id/set-profile'
|
||||||
| '/columns'
|
| '/columns'
|
||||||
| '/zap/$id'
|
| '/zap/$id'
|
||||||
| '/new-account/connect'
|
| '/new-account/connect'
|
||||||
@@ -795,7 +794,6 @@ export interface FileRouteTypes {
|
|||||||
| '/columns/create-newsfeed'
|
| '/columns/create-newsfeed'
|
||||||
| '/columns/global'
|
| '/columns/global'
|
||||||
| '/settings/$id/general'
|
| '/settings/$id/general'
|
||||||
| '/settings/$id/profile'
|
|
||||||
| '/settings/$id/relay'
|
| '/settings/$id/relay'
|
||||||
| '/settings/$id/wallet'
|
| '/settings/$id/wallet'
|
||||||
| '/columns/onboarding'
|
| '/columns/onboarding'
|
||||||
@@ -818,6 +816,7 @@ export interface FileRouteTypes {
|
|||||||
| '/new'
|
| '/new'
|
||||||
| '/$id/set-group'
|
| '/$id/set-group'
|
||||||
| '/$id/set-interest'
|
| '/$id/set-interest'
|
||||||
|
| '/$id/set-profile'
|
||||||
| '/columns'
|
| '/columns'
|
||||||
| '/zap/$id'
|
| '/zap/$id'
|
||||||
| '/new-account/connect'
|
| '/new-account/connect'
|
||||||
@@ -829,7 +828,6 @@ export interface FileRouteTypes {
|
|||||||
| '/columns/create-newsfeed'
|
| '/columns/create-newsfeed'
|
||||||
| '/columns/global'
|
| '/columns/global'
|
||||||
| '/settings/$id/general'
|
| '/settings/$id/general'
|
||||||
| '/settings/$id/profile'
|
|
||||||
| '/settings/$id/relay'
|
| '/settings/$id/relay'
|
||||||
| '/settings/$id/wallet'
|
| '/settings/$id/wallet'
|
||||||
| '/columns/onboarding'
|
| '/columns/onboarding'
|
||||||
@@ -853,6 +851,7 @@ export interface FileRouteTypes {
|
|||||||
| '/new'
|
| '/new'
|
||||||
| '/$id/set-group'
|
| '/$id/set-group'
|
||||||
| '/$id/set-interest'
|
| '/$id/set-interest'
|
||||||
|
| '/$id/set-profile'
|
||||||
| '/columns'
|
| '/columns'
|
||||||
| '/columns/_layout'
|
| '/columns/_layout'
|
||||||
| '/zap/$id'
|
| '/zap/$id'
|
||||||
@@ -865,7 +864,6 @@ export interface FileRouteTypes {
|
|||||||
| '/columns/_layout/create-newsfeed'
|
| '/columns/_layout/create-newsfeed'
|
||||||
| '/columns/_layout/global'
|
| '/columns/_layout/global'
|
||||||
| '/settings/$id/general'
|
| '/settings/$id/general'
|
||||||
| '/settings/$id/profile'
|
|
||||||
| '/settings/$id/relay'
|
| '/settings/$id/relay'
|
||||||
| '/settings/$id/wallet'
|
| '/settings/$id/wallet'
|
||||||
| '/columns/_layout/onboarding'
|
| '/columns/_layout/onboarding'
|
||||||
@@ -891,6 +889,7 @@ export interface RootRouteChildren {
|
|||||||
NewLazyRoute: typeof NewLazyRoute
|
NewLazyRoute: typeof NewLazyRoute
|
||||||
IdSetGroupRoute: typeof IdSetGroupRoute
|
IdSetGroupRoute: typeof IdSetGroupRoute
|
||||||
IdSetInterestRoute: typeof IdSetInterestRoute
|
IdSetInterestRoute: typeof IdSetInterestRoute
|
||||||
|
IdSetProfileRoute: typeof IdSetProfileRoute
|
||||||
ColumnsRoute: typeof ColumnsRouteWithChildren
|
ColumnsRoute: typeof ColumnsRouteWithChildren
|
||||||
ZapIdRoute: typeof ZapIdRoute
|
ZapIdRoute: typeof ZapIdRoute
|
||||||
NewAccountConnectLazyRoute: typeof NewAccountConnectLazyRoute
|
NewAccountConnectLazyRoute: typeof NewAccountConnectLazyRoute
|
||||||
@@ -906,6 +905,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
NewLazyRoute: NewLazyRoute,
|
NewLazyRoute: NewLazyRoute,
|
||||||
IdSetGroupRoute: IdSetGroupRoute,
|
IdSetGroupRoute: IdSetGroupRoute,
|
||||||
IdSetInterestRoute: IdSetInterestRoute,
|
IdSetInterestRoute: IdSetInterestRoute,
|
||||||
|
IdSetProfileRoute: IdSetProfileRoute,
|
||||||
ColumnsRoute: ColumnsRouteWithChildren,
|
ColumnsRoute: ColumnsRouteWithChildren,
|
||||||
ZapIdRoute: ZapIdRoute,
|
ZapIdRoute: ZapIdRoute,
|
||||||
NewAccountConnectLazyRoute: NewAccountConnectLazyRoute,
|
NewAccountConnectLazyRoute: NewAccountConnectLazyRoute,
|
||||||
@@ -932,6 +932,7 @@ export const routeTree = rootRoute
|
|||||||
"/new",
|
"/new",
|
||||||
"/$id/set-group",
|
"/$id/set-group",
|
||||||
"/$id/set-interest",
|
"/$id/set-interest",
|
||||||
|
"/$id/set-profile",
|
||||||
"/columns",
|
"/columns",
|
||||||
"/zap/$id",
|
"/zap/$id",
|
||||||
"/new-account/connect",
|
"/new-account/connect",
|
||||||
@@ -959,6 +960,9 @@ export const routeTree = rootRoute
|
|||||||
"/$id/set-interest": {
|
"/$id/set-interest": {
|
||||||
"filePath": "$id.set-interest.tsx"
|
"filePath": "$id.set-interest.tsx"
|
||||||
},
|
},
|
||||||
|
"/$id/set-profile": {
|
||||||
|
"filePath": "$id.set-profile.tsx"
|
||||||
|
},
|
||||||
"/columns": {
|
"/columns": {
|
||||||
"filePath": "columns",
|
"filePath": "columns",
|
||||||
"children": [
|
"children": [
|
||||||
@@ -1001,7 +1005,6 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "settings.$id.lazy.tsx",
|
"filePath": "settings.$id.lazy.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/settings/$id/general",
|
"/settings/$id/general",
|
||||||
"/settings/$id/profile",
|
|
||||||
"/settings/$id/relay",
|
"/settings/$id/relay",
|
||||||
"/settings/$id/wallet"
|
"/settings/$id/wallet"
|
||||||
]
|
]
|
||||||
@@ -1029,10 +1032,6 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "settings.$id/general.tsx",
|
"filePath": "settings.$id/general.tsx",
|
||||||
"parent": "/settings/$id"
|
"parent": "/settings/$id"
|
||||||
},
|
},
|
||||||
"/settings/$id/profile": {
|
|
||||||
"filePath": "settings.$id/profile.tsx",
|
|
||||||
"parent": "/settings/$id"
|
|
||||||
},
|
|
||||||
"/settings/$id/relay": {
|
"/settings/$id/relay": {
|
||||||
"filePath": "settings.$id/relay.tsx",
|
"filePath": "settings.$id/relay.tsx",
|
||||||
"parent": "/settings/$id"
|
"parent": "/settings/$id"
|
||||||
|
|||||||
230
src/routes/$id.set-profile.lazy.tsx
Normal file
230
src/routes/$id.set-profile.lazy.tsx
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
|
import { cn, upload } from "@/commons";
|
||||||
|
import { Spinner } from "@/components";
|
||||||
|
import type { Metadata } from "@/types";
|
||||||
|
import { Plus } from "@phosphor-icons/react";
|
||||||
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
type ReactNode,
|
||||||
|
type SetStateAction,
|
||||||
|
useState,
|
||||||
|
useTransition,
|
||||||
|
} from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/$id/set-profile")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const { profile, queryClient } = Route.useRouteContext();
|
||||||
|
const { register, handleSubmit } = useForm({ defaultValues: profile });
|
||||||
|
|
||||||
|
const [picture, setPicture] = useState<string>("");
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const onSubmit = (data: Metadata) => {
|
||||||
|
startTransition(async () => {
|
||||||
|
const signer = await commands.hasSigner(id);
|
||||||
|
|
||||||
|
if (signer.status === "ok") {
|
||||||
|
if (!signer.data) {
|
||||||
|
const res = await commands.setSigner(id);
|
||||||
|
|
||||||
|
if (res.status === "error") {
|
||||||
|
await message(res.error, { kind: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await message(signer.error, { kind: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProfile: Metadata = { ...profile, ...data, picture };
|
||||||
|
const res = await commands.setProfile(JSON.stringify(newProfile));
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
// Invalidate cache
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["profile", id],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close current popup
|
||||||
|
await getCurrentWindow().close();
|
||||||
|
} else {
|
||||||
|
await message(res.error, { title: "Profile", kind: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col size-full">
|
||||||
|
<div data-tauri-drag-region className="shrink-0 h-11" />
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="min-h-0 flex-1 flex flex-col mb-0"
|
||||||
|
>
|
||||||
|
<div className="shrink-0 h-20 flex items-center gap-3 p-3 border-b border-black/5 dark:border-white/5">
|
||||||
|
<div className="relative rounded-full size-12 bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200">
|
||||||
|
{profile.picture ? (
|
||||||
|
<img
|
||||||
|
src={picture || profile.picture}
|
||||||
|
alt="avatar"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
className="absolute inset-0 z-10 object-cover size-12 rounded-full"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<AvatarUploader
|
||||||
|
setPicture={setPicture}
|
||||||
|
className="absolute inset-0 z-20 flex items-center justify-center size-full text-white rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||||
|
>
|
||||||
|
<Plus className="size-5" />
|
||||||
|
</AvatarUploader>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">{profile.display_name}</div>
|
||||||
|
<div className="leading-tight text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
{profile.nip05?.startsWith("_")
|
||||||
|
? profile.nip05.replace("_@", "")
|
||||||
|
: profile.nip05}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="inline-flex items-center justify-center w-28 h-8 px-2 text-xs font-semibold text-white bg-blue-500 rounded-full hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner className="size-4" /> : "Update Profile"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="min-h-0 flex-1 overflow-hidden"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport className="bg-white dark:bg-black h-full p-3">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col w-full gap-1.5">
|
||||||
|
<label htmlFor="display_name" className="text-sm font-medium">
|
||||||
|
Display Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
{...register("display_name")}
|
||||||
|
spellCheck={false}
|
||||||
|
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-full gap-1.5">
|
||||||
|
<label htmlFor="name" className="text-sm font-medium">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
{...register("name")}
|
||||||
|
spellCheck={false}
|
||||||
|
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-full gap-1.5">
|
||||||
|
<label htmlFor="website" className="text-sm font-medium">
|
||||||
|
Website
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
{...register("website")}
|
||||||
|
spellCheck={false}
|
||||||
|
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-full gap-1.5">
|
||||||
|
<label htmlFor="banner" className="text-sm font-medium">
|
||||||
|
Cover
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
{...register("banner")}
|
||||||
|
spellCheck={false}
|
||||||
|
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-full gap-1.5">
|
||||||
|
<label htmlFor="nip05" className="text-sm font-medium">
|
||||||
|
NIP-05
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
{...register("nip05")}
|
||||||
|
spellCheck={false}
|
||||||
|
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-full gap-1.5">
|
||||||
|
<label htmlFor="lnaddress" className="text-sm font-medium">
|
||||||
|
Lightning Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
{...register("lud16")}
|
||||||
|
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarUploader({
|
||||||
|
setPicture,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
setPicture: Dispatch<SetStateAction<string>>;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const uploadAvatar = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
const image = await upload();
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
setPicture(image);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await message(String(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => uploadAvatar()}
|
||||||
|
className={cn("size-4", className)}
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner className="size-4" /> : children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type Profile, commands } from "@/commands.gen";
|
import { type Profile, commands } from "@/commands.gen";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings/$id/profile")({
|
export const Route = createFileRoute("/$id/set-profile")({
|
||||||
beforeLoad: async ({ params }) => {
|
beforeLoad: async ({ params }) => {
|
||||||
const res = await commands.getProfile(params.id);
|
const res = await commands.getProfile(params.id);
|
||||||
|
|
||||||
@@ -1,20 +1,38 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
import { Spinner } from "@/components";
|
import { Spinner } from "@/components";
|
||||||
import type { Metadata, NostrEvent } from "@/types";
|
import type { Metadata, NostrEvent } from "@/types";
|
||||||
import type { QueryClient } from "@tanstack/react-query";
|
import { type QueryClient, queryOptions } from "@tanstack/react-query";
|
||||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import type { OsType } from "@tauri-apps/plugin-os";
|
import type { OsType } from "@tauri-apps/plugin-os";
|
||||||
|
import type { Store } from "@tauri-apps/plugin-store";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
interface RouterContext {
|
interface RouterContext {
|
||||||
|
store: Store;
|
||||||
queryClient: QueryClient;
|
queryClient: QueryClient;
|
||||||
platform: OsType;
|
platform: OsType;
|
||||||
accounts?: string[];
|
accounts?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const settingsQueryOptions = queryOptions({
|
||||||
|
queryKey: ["settings"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await commands.getAppSettings();
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
return res.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
pendingComponent: Pending,
|
pendingComponent: Pending,
|
||||||
|
loader: ({ context }) =>
|
||||||
|
context.queryClient.ensureQueryData(settingsQueryOptions),
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { appColumns, cn } from "@/commons";
|
import { cn } from "@/commons";
|
||||||
import { PublishIcon } from "@/components";
|
import { PublishIcon } from "@/components";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import { LumeWindow } from "@/system";
|
import { LumeWindow } from "@/system";
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/_app")({
|
export const Route = createLazyFileRoute("/_app")({
|
||||||
@@ -107,7 +108,7 @@ function Account({ pubkey }: { pubkey: string }) {
|
|||||||
|
|
||||||
const items = await Promise.all([
|
const items = await Promise.all([
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Activate",
|
text: "Unlock",
|
||||||
enabled: !isActive || true,
|
enabled: !isActive || true,
|
||||||
action: async () => await commands.setSigner(pubkey),
|
action: async () => await commands.setSigner(pubkey),
|
||||||
}),
|
}),
|
||||||
@@ -116,10 +117,31 @@ function Account({ pubkey }: { pubkey: string }) {
|
|||||||
text: "View Profile",
|
text: "View Profile",
|
||||||
action: () => LumeWindow.openProfile(pubkey),
|
action: () => LumeWindow.openProfile(pubkey),
|
||||||
}),
|
}),
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Update Profile",
|
||||||
|
action: () =>
|
||||||
|
LumeWindow.openPopup(
|
||||||
|
`${pubkey}/set-profile`,
|
||||||
|
"Update Profile",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
PredefinedMenuItem.new({ item: "Separator" }),
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Copy Public Key",
|
text: "Copy Public Key",
|
||||||
action: async () => await writeText(pubkey),
|
action: async () => await writeText(pubkey),
|
||||||
}),
|
}),
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Copy Private Key",
|
||||||
|
action: async () => {
|
||||||
|
const res = await commands.getPrivateKey(pubkey);
|
||||||
|
if (res.status === "ok") {
|
||||||
|
await writeText(res.data);
|
||||||
|
} else {
|
||||||
|
await message(res.error, { kind: "error" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
PredefinedMenuItem.new({ item: "Separator" }),
|
PredefinedMenuItem.new({ item: "Separator" }),
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Settings",
|
text: "Settings",
|
||||||
@@ -127,20 +149,13 @@ function Account({ pubkey }: { pubkey: string }) {
|
|||||||
}),
|
}),
|
||||||
PredefinedMenuItem.new({ item: "Separator" }),
|
PredefinedMenuItem.new({ item: "Separator" }),
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Delete Account",
|
text: "Logout",
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const res = await commands.deleteAccount(pubkey);
|
const res = await commands.deleteAccount(pubkey);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
router.invalidate();
|
router.invalidate();
|
||||||
|
|
||||||
// Delete column associate with this account
|
|
||||||
appColumns.setState((prev) =>
|
|
||||||
prev.filter((col) =>
|
|
||||||
col.account ? col.account !== pubkey : col,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check remain account
|
// Check remain account
|
||||||
const newAccounts = context.accounts.filter(
|
const newAccounts = context.accounts.filter(
|
||||||
(account) => account !== pubkey,
|
(account) => account !== pubkey,
|
||||||
@@ -176,6 +191,7 @@ function Account({ pubkey }: { pubkey: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => showContextMenu(e)}
|
onClick={(e) => showContextMenu(e)}
|
||||||
|
onContextMenu={(e) => showContextMenu(e)}
|
||||||
className="h-10 relative"
|
className="h-10 relative"
|
||||||
>
|
>
|
||||||
<User.Provider pubkey={pubkey}>
|
<User.Provider pubkey={pubkey}>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { appColumns, displayNpub } from "@/commons";
|
import { displayNpub } from "@/commons";
|
||||||
import { Column, Spinner } from "@/components";
|
import { Column, Spinner } from "@/components";
|
||||||
import { LumeWindow } from "@/system";
|
import { LumeWindow } from "@/system";
|
||||||
import type { ColumnEvent, LumeColumn, Metadata } from "@/types";
|
import type { ColumnEvent, LumeColumn, Metadata } from "@/types";
|
||||||
import { ArrowLeft, ArrowRight, Plus } from "@phosphor-icons/react";
|
import { ArrowLeft, ArrowRight, Plus } from "@phosphor-icons/react";
|
||||||
import { createLazyFileRoute, useRouter } from "@tanstack/react-router";
|
import { createLazyFileRoute, useRouter } from "@tanstack/react-router";
|
||||||
import { useStore } from "@tanstack/react-store";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
@@ -26,10 +25,11 @@ export const Route = createLazyFileRoute("/_app/")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
|
const context = Route.useRouteContext();
|
||||||
const initialAppColumns = Route.useLoaderData();
|
const initialAppColumns = Route.useLoaderData();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const columns = useStore(appColumns, (state) => state);
|
|
||||||
|
|
||||||
|
const [columns, setColumns] = useState<LumeColumn[]>([]);
|
||||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||||
watchDrag: false,
|
watchDrag: false,
|
||||||
loop: false,
|
loop: false,
|
||||||
@@ -52,9 +52,9 @@ function Screen() {
|
|||||||
const res = await commands.closeColumn(label);
|
const res = await commands.closeColumn(label);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
appColumns.setState((prev) => prev.filter((t) => t.label !== label));
|
setColumns((prev) => prev.filter((t) => t.label !== label));
|
||||||
} else {
|
} else {
|
||||||
await message(res.error, { kind: "errror" });
|
await message(res.error, { kind: "error" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[columns],
|
[columns],
|
||||||
@@ -64,7 +64,7 @@ function Screen() {
|
|||||||
const exist = columns.find((col) => col.label === column.label);
|
const exist = columns.find((col) => col.label === column.label);
|
||||||
|
|
||||||
if (!exist) {
|
if (!exist) {
|
||||||
appColumns.setState((prev) => [column, ...prev]);
|
setColumns((prev) => [column, ...prev]);
|
||||||
|
|
||||||
if (emblaApi) {
|
if (emblaApi) {
|
||||||
emblaApi.scrollTo(0, true);
|
emblaApi.scrollTo(0, true);
|
||||||
@@ -85,7 +85,7 @@ function Screen() {
|
|||||||
if (direction === "left") newCols.splice(colIndex - 1, 0, existColumn);
|
if (direction === "left") newCols.splice(colIndex - 1, 0, existColumn);
|
||||||
if (direction === "right") newCols.splice(colIndex + 1, 0, existColumn);
|
if (direction === "right") newCols.splice(colIndex + 1, 0, existColumn);
|
||||||
|
|
||||||
appColumns.setState(() => newCols);
|
setColumns(() => newCols);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
150,
|
150,
|
||||||
@@ -100,11 +100,9 @@ function Screen() {
|
|||||||
const newCols = columns.slice();
|
const newCols = columns.slice();
|
||||||
newCols[currentColIndex] = updatedCol;
|
newCols[currentColIndex] = updatedCol;
|
||||||
|
|
||||||
appColumns.setState(() => newCols);
|
setColumns(() => newCols);
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
const reset = useDebouncedCallback(() => appColumns.setState(() => []), 150);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (emblaApi) {
|
if (emblaApi) {
|
||||||
emblaApi.on("scroll", emitScrollEvent);
|
emblaApi.on("scroll", emitScrollEvent);
|
||||||
@@ -119,7 +117,6 @@ function Screen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = listen<ColumnEvent>("columns", (data) => {
|
const unlisten = listen<ColumnEvent>("columns", (data) => {
|
||||||
if (data.payload.type === "reset") reset();
|
|
||||||
if (data.payload.type === "add") add(data.payload.column);
|
if (data.payload.type === "add") add(data.payload.column);
|
||||||
if (data.payload.type === "remove") remove(data.payload.label);
|
if (data.payload.type === "remove") remove(data.payload.label);
|
||||||
if (data.payload.type === "move")
|
if (data.payload.type === "move")
|
||||||
@@ -145,12 +142,14 @@ function Screen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialAppColumns) {
|
if (initialAppColumns) {
|
||||||
appColumns.setState(() => initialAppColumns);
|
setColumns(() => initialAppColumns);
|
||||||
}
|
}
|
||||||
}, [initialAppColumns]);
|
}, [initialAppColumns]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.localStorage.setItem("columns", JSON.stringify(columns));
|
(async () => {
|
||||||
|
await context.store.set("columns", JSON.stringify(columns));
|
||||||
|
})();
|
||||||
}, [columns]);
|
}, [columns]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { nanoid } from "nanoid";
|
|||||||
export const Route = createFileRoute("/_app/")({
|
export const Route = createFileRoute("/_app/")({
|
||||||
loader: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
const accounts = context.accounts;
|
const accounts = context.accounts;
|
||||||
const prevColumns = window.localStorage.getItem("columns");
|
const prev = await context.store.get("columns");
|
||||||
|
|
||||||
let initialAppColumns: LumeColumn[] = [];
|
let initialAppColumns: LumeColumn[] = [];
|
||||||
|
|
||||||
if (!prevColumns || prevColumns.length < 1) {
|
if (!prev) {
|
||||||
initialAppColumns.push({
|
initialAppColumns.push({
|
||||||
label: "onboarding",
|
label: "onboarding",
|
||||||
name: "Onboarding",
|
name: "Onboarding",
|
||||||
@@ -25,7 +25,7 @@ export const Route = createFileRoute("/_app/")({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const parsed: LumeColumn[] = JSON.parse(prevColumns);
|
const parsed: LumeColumn[] = JSON.parse(prev as string);
|
||||||
|
|
||||||
initialAppColumns = parsed.filter((item) =>
|
initialAppColumns = parsed.filter((item) =>
|
||||||
item.account ? context.accounts.includes(item.account) : item,
|
item.account ? context.accounts.includes(item.account) : item,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { decodeZapInvoice, formatCreatedAt } from "@/commons";
|
import { createdAt, decodeZapInvoice } from "@/commons";
|
||||||
import { Note, RepostIcon, Spinner, User } from "@/components";
|
import { Note, RepostIcon, Spinner, User } from "@/components";
|
||||||
import { LumeEvent, LumeWindow, useEvent } from "@/system";
|
import { LumeEvent, LumeWindow, useEvent } from "@/system";
|
||||||
import { Kind, type NostrEvent } from "@/types";
|
import { Kind, type NostrEvent } from "@/types";
|
||||||
@@ -300,7 +300,7 @@ function TextNote({ event }: { event: LumeEvent }) {
|
|||||||
<div className="flex items-baseline justify-between w-full">
|
<div className="flex items-baseline justify-between w-full">
|
||||||
<User.Name className="text-sm font-semibold leading-tight" />
|
<User.Name className="text-sm font-semibold leading-tight" />
|
||||||
<span className="text-sm leading-tight text-black/50 dark:text-white/50">
|
<span className="text-sm leading-tight text-black/50 dark:text-white/50">
|
||||||
{formatCreatedAt(event.created_at)}
|
{createdAt(event.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-baseline gap-1 text-xs">
|
<div className="inline-flex items-baseline gap-1 text-xs">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { replyTime, toLumeEvents } from "@/commons";
|
import { replyAt, toLumeEvents } from "@/commons";
|
||||||
import { Note, Spinner, User } from "@/components";
|
import { Note, Spinner, User } from "@/components";
|
||||||
import { Hashtag } from "@/components/note/mentions/hashtag";
|
import { Hashtag } from "@/components/note/mentions/hashtag";
|
||||||
import { MentionUser } from "@/components/note/mentions/user";
|
import { MentionUser } from "@/components/note/mentions/user";
|
||||||
@@ -155,7 +155,7 @@ const StoryEvent = memo(function StoryEvent({ event }: { event: LumeEvent }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-between">
|
<div className="flex-1 flex items-center justify-between">
|
||||||
<span className="text-sm text-neutral-500">
|
<span className="text-sm text-neutral-500">
|
||||||
{replyTime(event.created_at)}
|
{replyAt(event.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<div className="invisible group-hover:visible flex items-center justify-end gap-3">
|
<div className="invisible group-hover:visible flex items-center justify-end gap-3">
|
||||||
<Note.Reply />
|
<Note.Reply />
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { type Settings, commands } from "@/commands.gen";
|
||||||
import { appSettings } from "@/commons";
|
|
||||||
import { Spinner } from "@/components";
|
import { Spinner } from "@/components";
|
||||||
import * as Switch from "@radix-ui/react-switch";
|
import * as Switch from "@radix-ui/react-switch";
|
||||||
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { useStore } from "@tanstack/react-store";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useCallback, useEffect, useState, useTransition } from "react";
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
type SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useTransition,
|
||||||
|
} from "react";
|
||||||
|
import { settingsQueryOptions } from "../__root";
|
||||||
|
|
||||||
type Theme = "auto" | "light" | "dark";
|
type Theme = "auto" | "light" | "dark";
|
||||||
|
|
||||||
@@ -15,7 +22,11 @@ export const Route = createLazyFileRoute("/settings/$id/general")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const [theme, setTheme] = useState<Theme>(null);
|
const settings = useSuspenseQuery(settingsQueryOptions);
|
||||||
|
const { queryClient } = Route.useRouteContext();
|
||||||
|
|
||||||
|
const [theme, setTheme] = useState<Theme>("auto");
|
||||||
|
const [newSettings, setNewSettings] = useState<Settings>();
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const changeTheme = useCallback(async (theme: string) => {
|
const changeTheme = useCallback(async (theme: string) => {
|
||||||
@@ -28,14 +39,14 @@ function Screen() {
|
|||||||
|
|
||||||
const updateSettings = () => {
|
const updateSettings = () => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const newSettings = JSON.stringify(appSettings.state);
|
const res = await commands.setAppSettings(JSON.stringify(newSettings));
|
||||||
const res = await commands.setUserSettings(newSettings);
|
|
||||||
|
|
||||||
if (res.status === "error") {
|
if (res.status === "ok") {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
await message(res.error, { kind: "error" });
|
await message(res.error, { kind: "error" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,6 +54,16 @@ function Screen() {
|
|||||||
invoke("plugin:theme|get_theme").then((data) => setTheme(data as Theme));
|
invoke("plugin:theme|get_theme").then((data) => setTheme(data as Theme));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings.status === "success") {
|
||||||
|
setNewSettings(settings.data);
|
||||||
|
}
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
if (!newSettings) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<div className="flex flex-col gap-6 px-3 pb-3">
|
<div className="flex flex-col gap-6 px-3 pb-3">
|
||||||
@@ -51,21 +72,22 @@ function Screen() {
|
|||||||
General
|
General
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-col px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
|
<div className="flex flex-col px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
|
||||||
<Setting
|
|
||||||
name="Relay Hint"
|
|
||||||
description="Use the relay hint if necessary."
|
|
||||||
label="use_relay_hint"
|
|
||||||
/>
|
|
||||||
<Setting
|
<Setting
|
||||||
name="Content Warning"
|
name="Content Warning"
|
||||||
description="Shows a warning for notes that have a content warning."
|
description="Shows a warning for notes that have a content warning."
|
||||||
label="content_warning"
|
label="content_warning"
|
||||||
|
checked={newSettings.content_warning}
|
||||||
|
setNewSettings={setNewSettings}
|
||||||
/>
|
/>
|
||||||
|
{/*
|
||||||
<Setting
|
<Setting
|
||||||
name="Trusted Only"
|
name="Trusted Only"
|
||||||
description="Only shows note's replies from your inner circle."
|
description="Only shows note's replies from your inner circle."
|
||||||
label="trusted_only"
|
label="trusted_only"
|
||||||
|
newSettings={newSettings}
|
||||||
|
setNewSettings={setNewSettings}
|
||||||
/>
|
/>
|
||||||
|
*/}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -94,19 +116,25 @@ function Screen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Setting
|
<Setting
|
||||||
name="Transparent Effect"
|
name="Show Avatar"
|
||||||
description="Use native window transparent effect."
|
description="Shows the user avatar."
|
||||||
label="transparent"
|
label="display_avatar"
|
||||||
|
checked={newSettings.display_avatar}
|
||||||
|
setNewSettings={setNewSettings}
|
||||||
/>
|
/>
|
||||||
<Setting
|
<Setting
|
||||||
name="Show Zap Button"
|
name="Show Zap Button"
|
||||||
description="Shows the Zap button when viewing a note."
|
description="Shows the Zap button when viewing a note."
|
||||||
label="display_zap_button"
|
label="display_zap_button"
|
||||||
|
checked={newSettings.display_zap_button}
|
||||||
|
setNewSettings={setNewSettings}
|
||||||
/>
|
/>
|
||||||
<Setting
|
<Setting
|
||||||
name="Show Repost Button"
|
name="Show Repost Button"
|
||||||
description="Shows the Repost button when viewing a note."
|
description="Shows the Repost button when viewing a note."
|
||||||
label="display_repost_button"
|
label="display_repost_button"
|
||||||
|
checked={newSettings.display_repost_button}
|
||||||
|
setNewSettings={setNewSettings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,25 +144,23 @@ function Screen() {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-col px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
|
<div className="flex flex-col px-3 divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl">
|
||||||
<Setting
|
<Setting
|
||||||
name="Proxy"
|
name="Resize Service"
|
||||||
description="Set proxy address."
|
description="Use weserv for resize image on-the-fly."
|
||||||
label="proxy"
|
label="resize_service"
|
||||||
|
checked={newSettings.resize_service}
|
||||||
|
setNewSettings={setNewSettings}
|
||||||
/>
|
/>
|
||||||
<Setting
|
<Setting
|
||||||
name="Image Resize Service"
|
name="Show Remote Media"
|
||||||
description="Use weserv/images for resize image on-the-fly."
|
description="Automatically load remote media."
|
||||||
label="image_resize_service"
|
|
||||||
/>
|
|
||||||
<Setting
|
|
||||||
name="Load Remote Media"
|
|
||||||
description="View the remote media directly."
|
|
||||||
label="display_media"
|
label="display_media"
|
||||||
|
checked={newSettings.display_media}
|
||||||
|
setNewSettings={setNewSettings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky bottom-0 left-0 w-full h-16 flex items-center justify-end px-3">
|
<div className="w-full h-16 flex items-center justify-end px-3">
|
||||||
<div className="absolute left-0 bottom-0 w-full h-11 gradient-mask-t-0 bg-neutral-100 dark:bg-neutral-900" />
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateSettings()}
|
onClick={() => updateSettings()}
|
||||||
@@ -151,33 +177,37 @@ function Setting({
|
|||||||
label,
|
label,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
checked,
|
||||||
|
setNewSettings,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
checked: boolean;
|
||||||
|
setNewSettings: Dispatch<SetStateAction<Settings | undefined>>;
|
||||||
}) {
|
}) {
|
||||||
const state = useStore(appSettings, (state) => state[label]);
|
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
appSettings.setState((state) => {
|
setNewSettings((state) => {
|
||||||
return {
|
if (state) {
|
||||||
...state,
|
return {
|
||||||
[label]: !state[label],
|
...state,
|
||||||
};
|
[label]: !state[label],
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start justify-between w-full gap-4 py-3">
|
<div className="flex items-start justify-between w-full gap-4 py-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-medium">{name}</h3>
|
<h3 className="text-sm font-medium">{name}</h3>
|
||||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
<p className="text-xs text-neutral-700 dark:text-neutral-300">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end w-36 shrink-0">
|
<div className="flex justify-end w-36 shrink-0">
|
||||||
<Switch.Root
|
<Switch.Root
|
||||||
checked={state}
|
checked={checked}
|
||||||
onClick={() => toggle()}
|
onClick={() => toggle()}
|
||||||
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,17 +1,3 @@
|
|||||||
import { commands } from "@/commands.gen";
|
|
||||||
import { appSettings } from "@/commons";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings/$id/general")({
|
export const Route = createFileRoute("/settings/$id/general")();
|
||||||
beforeLoad: async () => {
|
|
||||||
const res = await commands.getUserSettings();
|
|
||||||
|
|
||||||
if (res.status === "ok") {
|
|
||||||
appSettings.setState((state) => {
|
|
||||||
return { ...state, ...res.data };
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error(res.error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
import { type Profile, commands } from "@/commands.gen";
|
|
||||||
import { cn, upload } from "@/commons";
|
|
||||||
import { Spinner } from "@/components";
|
|
||||||
import { Plus } from "@phosphor-icons/react";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
|
||||||
import {
|
|
||||||
type Dispatch,
|
|
||||||
type ReactNode,
|
|
||||||
type SetStateAction,
|
|
||||||
useState,
|
|
||||||
useTransition,
|
|
||||||
} from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/settings/$id/profile")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const { profile } = Route.useRouteContext();
|
|
||||||
const { register, handleSubmit } = useForm({ defaultValues: profile });
|
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const [picture, setPicture] = useState<string>("");
|
|
||||||
|
|
||||||
const onSubmit = (data: Profile) => {
|
|
||||||
startTransition(async () => {
|
|
||||||
const newProfile: Profile = { ...profile, ...data, picture };
|
|
||||||
const res = await commands.setProfile(newProfile);
|
|
||||||
|
|
||||||
if (res.status === "error") {
|
|
||||||
await message(res.error, { title: "Profile", kind: "error" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex flex-col gap-6 px-3 pb-3">
|
|
||||||
<div className="flex items-center flex-1 h-full gap-3">
|
|
||||||
<div className="relative rounded-full size-20 bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200">
|
|
||||||
{profile.picture ? (
|
|
||||||
<img
|
|
||||||
src={picture || profile.picture}
|
|
||||||
alt="avatar"
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
className="absolute inset-0 z-10 object-cover size-20 rounded-full"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<AvatarUploader
|
|
||||||
setPicture={setPicture}
|
|
||||||
className="absolute inset-0 z-20 flex items-center justify-center size-full text-white rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
|
||||||
>
|
|
||||||
<Plus className="size-5" />
|
|
||||||
</AvatarUploader>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg font-semibold">{profile.display_name}</div>
|
|
||||||
<div className="text-neutral-700 dark:text-neutral-300">
|
|
||||||
{profile.nip05}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<PrivkeyButton />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-3 mb-0"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col w-full gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="display_name"
|
|
||||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
|
||||||
>
|
|
||||||
Display Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="display_name"
|
|
||||||
{...register("display_name")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-full gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="name"
|
|
||||||
{...register("name")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-full gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="website"
|
|
||||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
|
||||||
>
|
|
||||||
Website
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="website"
|
|
||||||
type="url"
|
|
||||||
{...register("website")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-full gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="banner"
|
|
||||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
|
||||||
>
|
|
||||||
Cover
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="banner"
|
|
||||||
type="url"
|
|
||||||
{...register("banner")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-full gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="nip05"
|
|
||||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
|
||||||
>
|
|
||||||
NIP-05
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="nip05"
|
|
||||||
type="email"
|
|
||||||
{...register("nip05")}
|
|
||||||
spellCheck={false}
|
|
||||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-full gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="lnaddress"
|
|
||||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
|
||||||
>
|
|
||||||
Lightning Address
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="lnaddress"
|
|
||||||
type="email"
|
|
||||||
{...register("lud16")}
|
|
||||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isPending}
|
|
||||||
className="inline-flex items-center justify-center w-32 px-2 text-sm font-medium text-white bg-blue-500 rounded-lg h-9 hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isPending ? <Spinner className="size-4" /> : "Update Profile"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PrivkeyButton() {
|
|
||||||
const { id } = Route.useParams();
|
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const [isCopy, setIsCopy] = useState(false);
|
|
||||||
|
|
||||||
const copyPrivateKey = () => {
|
|
||||||
startTransition(async () => {
|
|
||||||
const res = await commands.getPrivateKey(id);
|
|
||||||
|
|
||||||
if (res.status === "ok") {
|
|
||||||
await writeText(res.data);
|
|
||||||
setIsCopy(true);
|
|
||||||
} else {
|
|
||||||
await message(res.error, { kind: "error" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => copyPrivateKey()}
|
|
||||||
className="inline-flex items-center justify-center px-3 text-sm font-medium text-blue-500 bg-blue-100 border border-blue-300 rounded-full h-7 hover:bg-blue-200 dark:bg-blue-900 dark:border-blue-800 dark:text-blue-300 dark:hover:bg-blue-800"
|
|
||||||
>
|
|
||||||
{isPending ? (
|
|
||||||
<Spinner className="size-4" />
|
|
||||||
) : isCopy ? (
|
|
||||||
"Copied"
|
|
||||||
) : (
|
|
||||||
"Copy Private Key"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarUploader({
|
|
||||||
setPicture,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
setPicture: Dispatch<SetStateAction<string>>;
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const uploadAvatar = () => {
|
|
||||||
startTransition(async () => {
|
|
||||||
try {
|
|
||||||
const image = await upload();
|
|
||||||
setPicture(image);
|
|
||||||
} catch (e) {
|
|
||||||
await message(String(e));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => uploadAvatar()}
|
|
||||||
className={cn("size-4", className)}
|
|
||||||
>
|
|
||||||
{isPending ? <Spinner className="size-4" /> : children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -97,21 +97,12 @@ export const LumeWindow = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
openEditor: async (reply_to?: string, quote?: string) => {
|
openEditor: async (reply_to?: string, quote?: string) => {
|
||||||
let url: string;
|
|
||||||
|
|
||||||
if (reply_to) {
|
|
||||||
url = `/new-post?reply_to=${reply_to}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quote?.length) {
|
|
||||||
url = `/new-post?quote=${quote}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reply_to?.length && !quote?.length) {
|
|
||||||
url = "/new-post";
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = `editor-${reply_to ? reply_to : 0}`;
|
const label = `editor-${reply_to ? reply_to : 0}`;
|
||||||
|
const url = reply_to
|
||||||
|
? `/new-post?reply_to=${reply_to}`
|
||||||
|
: quote?.length
|
||||||
|
? `/new-post?quote=${quote}`
|
||||||
|
: "/new-post";
|
||||||
const query = await commands.openWindow({
|
const query = await commands.openWindow({
|
||||||
label,
|
label,
|
||||||
url,
|
url,
|
||||||
@@ -145,7 +136,7 @@ export const LumeWindow = {
|
|||||||
hidden_title: true,
|
hidden_title: true,
|
||||||
closable: true,
|
closable: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (account) {
|
||||||
await LumeWindow.openSettings(account, "wallet");
|
await LumeWindow.openSettings(account, "wallet");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user