wip: local relay

This commit is contained in:
2024-09-25 09:59:54 +07:00
parent 3d5085785b
commit bacfaed48a
6 changed files with 123 additions and 127 deletions

129
src-tauri/Cargo.lock generated
View File

@@ -19,6 +19,7 @@ dependencies = [
"keyring-search", "keyring-search",
"linkify", "linkify",
"monitor", "monitor",
"nostr-relay-builder",
"nostr-sdk", "nostr-sdk",
"objc", "objc",
"rand 0.8.5", "rand 0.8.5",
@@ -346,9 +347,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.82" version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -370,8 +371,7 @@ dependencies = [
[[package]] [[package]]
name = "async-wsocket" name = "async-wsocket"
version = "0.9.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/shadowylab/async-wsocket?rev=4d6a5b1780e65dc657ac36e5990a97c10feef072#4d6a5b1780e65dc657ac36e5990a97c10feef072"
checksum = "5c0984bead67f20366bc8dd46018dfbe189b67eeefb0e5b86b9eade18d7c3c3b"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"futures", "futures",
@@ -621,7 +621,7 @@ dependencies = [
[[package]] [[package]]
name = "border" name = "border"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#994bb36b860f91e8a6e9990f203031f4346a29b7" source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#e2890ea09160354103406e1e2a908a3816a2a89c"
dependencies = [ dependencies = [
"cocoa 0.25.0", "cocoa 0.25.0",
"color", "color",
@@ -929,7 +929,7 @@ dependencies = [
[[package]] [[package]]
name = "color" name = "color"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#994bb36b860f91e8a6e9990f203031f4346a29b7" source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#e2890ea09160354103406e1e2a908a3816a2a89c"
dependencies = [ dependencies = [
"cocoa 0.25.0", "cocoa 0.25.0",
"objc", "objc",
@@ -2438,9 +2438,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@@ -2451,7 +2451,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio", "tokio",
"tower",
"tower-service", "tower-service",
"tracing", "tracing",
] ]
@@ -2819,9 +2818,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.158" version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]] [[package]]
name = "libloading" name = "libloading"
@@ -3088,7 +3087,7 @@ dependencies = [
[[package]] [[package]]
name = "monitor" name = "monitor"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#994bb36b860f91e8a6e9990f203031f4346a29b7" source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#e2890ea09160354103406e1e2a908a3816a2a89c"
dependencies = [ dependencies = [
"cocoa 0.25.0", "cocoa 0.25.0",
"core-foundation 0.9.4", "core-foundation 0.9.4",
@@ -3226,7 +3225,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#8c1e2a85c5476f9aa0848a39676632833bc8446d" source = "git+https://github.com/rust-nostr/nostr#e99e56e712ac4f3830390619e52ccd11a2b41f9e"
dependencies = [ dependencies = [
"aes", "aes",
"base64 0.22.1", "base64 0.22.1",
@@ -3256,7 +3255,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#8c1e2a85c5476f9aa0848a39676632833bc8446d" source = "git+https://github.com/rust-nostr/nostr#e99e56e712ac4f3830390619e52ccd11a2b41f9e"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"flatbuffers", "flatbuffers",
@@ -3270,7 +3269,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#8c1e2a85c5476f9aa0848a39676632833bc8446d" source = "git+https://github.com/rust-nostr/nostr#e99e56e712ac4f3830390619e52ccd11a2b41f9e"
dependencies = [ dependencies = [
"heed", "heed",
"nostr", "nostr",
@@ -3280,10 +3279,25 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "nostr-relay-builder"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#e99e56e712ac4f3830390619e52ccd11a2b41f9e"
dependencies = [
"async-utility",
"async-wsocket",
"atomic-destructor",
"nostr",
"nostr-database",
"thiserror",
"tokio",
"tracing",
]
[[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#8c1e2a85c5476f9aa0848a39676632833bc8446d" source = "git+https://github.com/rust-nostr/nostr#e99e56e712ac4f3830390619e52ccd11a2b41f9e"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3301,7 +3315,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#8c1e2a85c5476f9aa0848a39676632833bc8446d" source = "git+https://github.com/rust-nostr/nostr#e99e56e712ac4f3830390619e52ccd11a2b41f9e"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"atomic-destructor", "atomic-destructor",
@@ -3321,7 +3335,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#8c1e2a85c5476f9aa0848a39676632833bc8446d" source = "git+https://github.com/rust-nostr/nostr#e99e56e712ac4f3830390619e52ccd11a2b41f9e"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3334,7 +3348,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#8c1e2a85c5476f9aa0848a39676632833bc8446d" source = "git+https://github.com/rust-nostr/nostr#e99e56e712ac4f3830390619e52ccd11a2b41f9e"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"nostr", "nostr",
@@ -3467,7 +3481,7 @@ dependencies = [
[[package]] [[package]]
name = "nwc" name = "nwc"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#8c1e2a85c5476f9aa0848a39676632833bc8446d" source = "git+https://github.com/rust-nostr/nostr#e99e56e712ac4f3830390619e52ccd11a2b41f9e"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3981,26 +3995,6 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.14"
@@ -4026,9 +4020,9 @@ dependencies = [
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]] [[package]]
name = "plist" name = "plist"
@@ -4382,9 +4376,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.4" version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
] ]
@@ -4765,9 +4759,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.11.1" version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -5322,9 +5316,9 @@ dependencies = [
[[package]] [[package]]
name = "tao" name = "tao"
version = "0.30.1" version = "0.30.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82e7ede56f9ef03a0bb384c7b2bed4f3985ee7f3f79ec887c50d8466eec21096" checksum = "06e48d7c56b3f7425d061886e8ce3b6acfab1993682ed70bef50fd133d721ee6"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cocoa 0.26.0", "cocoa 0.26.0",
@@ -5372,9 +5366,9 @@ dependencies = [
[[package]] [[package]]
name = "tar" name = "tar"
version = "0.4.41" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020"
dependencies = [ dependencies = [
"filetime", "filetime",
"libc", "libc",
@@ -5554,8 +5548,8 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-decorum" name = "tauri-plugin-decorum"
version = "1.1.0" version = "1.1.1"
source = "git+https://github.com/reyamir/tauri-plugin-decorum?branch=fix/tauri-latest#f2c3a59eb76e0496c8c28245ed354f882e8f5838" source = "git+https://github.com/clearlysid/tauri-plugin-decorum.git#278e34378116ea80f7b6176ae85cf6aa26c631eb"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cocoa 0.25.0", "cocoa 0.25.0",
@@ -5951,18 +5945,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.63" version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.63" version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6198,27 +6192,6 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@@ -6761,7 +6734,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

View File

@@ -29,11 +29,12 @@ tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-updater = "2.0.0-rc" tauri-plugin-updater = "2.0.0-rc"
tauri-plugin-upload = "2.0.0-rc" tauri-plugin-upload = "2.0.0-rc"
tauri-plugin-store = "2.0.0-rc" tauri-plugin-store = "2.0.0-rc"
tauri-plugin-decorum = { git = "https://github.com/reyamir/tauri-plugin-decorum", branch = "fix/tauri-latest" } tauri-plugin-decorum = { git = "https://github.com/clearlysid/tauri-plugin-decorum.git" }
tauri-plugin-prevent-default = "0.4" tauri-plugin-prevent-default = "0.4"
tauri-specta = { version = "2.0.0-rc.15", features = ["derive", "typescript"] } tauri-specta = { version = "2.0.0-rc.15", features = ["derive", "typescript"] }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb"] } nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb"] }
nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" }
specta = "^2.0.0-rc.20" specta = "^2.0.0-rc.20"
specta-typescript = "0.0.7" specta-typescript = "0.0.7"

View File

@@ -256,7 +256,7 @@ pub async fn login(
}; };
// Connect to user's relay (NIP-65) // Connect to user's relay (NIP-65)
init_nip65(client).await; init_nip65(client, &public_key).await;
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let state = handle.state::<Nostr>(); let state = handle.state::<Nostr>();

View File

@@ -252,26 +252,12 @@ pub fn create_event_tags(content: &str) -> Vec<Tag> {
tags tags
} }
pub async fn init_nip65(client: &Client) { pub async fn init_nip65(client: &Client, public_key: &str) {
let signer = match client.signer().await { let author = PublicKey::from_str(public_key).unwrap();
Ok(signer) => signer, let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1);
Err(e) => {
eprintln!("Failed to get signer: {:?}", e);
return;
}
};
let public_key = match signer.public_key().await {
Ok(public_key) => public_key,
Err(e) => {
eprintln!("Failed to get public key: {:?}", e);
return;
}
};
let filter = Filter::new() // client.add_relay("ws://127.0.0.1:1984").await.unwrap();
.author(public_key) // client.connect_relay("ws://127.0.0.1:1984").await.unwrap();
.kind(Kind::RelayList)
.limit(1);
if let Ok(events) = client if let Ok(events) = client
.get_events_of( .get_events_of(

View File

@@ -7,6 +7,7 @@
use border::WebviewWindowExt as BorderWebviewWindowExt; use border::WebviewWindowExt as BorderWebviewWindowExt;
use commands::{account::*, event::*, metadata::*, relay::*, window::*}; use commands::{account::*, event::*, metadata::*, relay::*, window::*};
use common::parse_event; use common::parse_event;
use nostr_relay_builder::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
@@ -175,6 +176,19 @@ fn main() {
let handle_clone_child = handle_clone.clone(); let handle_clone_child = handle_clone.clone();
let main_window = app.get_webview_window("main").unwrap(); let main_window = app.get_webview_window("main").unwrap();
let config_dir = handle
.path()
.app_config_dir()
.expect("Error: app config directory not found.");
let data_dir = handle
.path()
.app_data_dir()
.expect("Error: app data directory not found.");
let _ = fs::create_dir_all(&config_dir);
let _ = fs::create_dir_all(&data_dir);
// Set custom decoration for Windows // Set custom decoration for Windows
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
main_window.create_overlay_titlebar().unwrap(); main_window.create_overlay_titlebar().unwrap();
@@ -198,15 +212,8 @@ fn main() {
}); });
let client = tauri::async_runtime::block_on(async move { let client = tauri::async_runtime::block_on(async move {
// Create data folder if not exist
let dir = handle
.path()
.app_config_dir()
.expect("Error: app config directory not found.");
let _ = fs::create_dir_all(&dir);
// Setup database // Setup database
let database = NostrLMDB::open(dir.join("nostr-lmdb")) let database = NostrLMDB::open(config_dir.join("nostr-lmdb"))
.expect("Error: cannot create database."); .expect("Error: cannot create database.");
// Config // Config
@@ -286,27 +293,39 @@ fn main() {
SubKind::Subscribe => { SubKind::Subscribe => {
let subscription_id = SubscriptionId::new(payload.label); let subscription_id = SubscriptionId::new(payload.label);
let filter = if let Some(id) = payload.event_id { match payload.event_id {
let event_id = EventId::from_str(&id).unwrap(); Some(id) => {
let event_id = EventId::from_str(&id).unwrap();
let filter =
Filter::new().event(event_id).since(Timestamp::now());
Filter::new().event(event_id).since(Timestamp::now()) if let Err(e) = client
} else { .subscribe_with_id(subscription_id, vec![filter], None)
let contact_list = state.contact_list.lock().await; .await
let authors: Vec<PublicKey> = {
contact_list.iter().map(|f| f.public_key).collect(); println!("Subscription error: {}", e)
}
}
None => {
let contact_list = state.contact_list.lock().await;
if !contact_list.is_empty() {
let authors: Vec<PublicKey> =
contact_list.iter().map(|f| f.public_key).collect();
Filter::new() let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost]) .kinds(vec![Kind::TextNote, Kind::Repost])
.authors(authors) .authors(authors)
.since(Timestamp::now()) .since(Timestamp::now());
if let Err(e) = client
.subscribe_with_id(subscription_id, vec![filter], None)
.await
{
println!("Subscription error: {}", e)
}
}
}
}; };
if let Err(e) = client
.subscribe_with_id(subscription_id, vec![filter], None)
.await
{
println!("Subscription error: {}", e)
}
} }
SubKind::Unsubscribe => { SubKind::Unsubscribe => {
let subscription_id = SubscriptionId::new(payload.label); let subscription_id = SubscriptionId::new(payload.label);
@@ -316,6 +335,22 @@ fn main() {
}); });
}); });
// Run local relay thread
tauri::async_runtime::spawn(async move {
let database = NostrLMDB::open(data_dir.join("local-relay"))
.expect("Error: cannot create database.");
let builder = RelayBuilder::default().database(database).port(1984);
if let Ok(relay) = LocalRelay::run(builder).await {
println!("Running local relay: {}", relay.url())
}
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
}
});
// Run notification thread
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let state = handle_clone.state::<Nostr>(); let state = handle_clone.state::<Nostr>();
let client = &state.client; let client = &state.client;

View File

@@ -128,11 +128,12 @@ function Screen() {
</div> </div>
</div> </div>
</div> </div>
<div className="sticky bottom-0 left-0 w-full h-11 flex items-center justify-end px-3 bg-white/20 dark:bg-black-20 backdrop-blur-md border-t border-black/5 dark:border-white/5"> <div className="sticky bottom-0 left-0 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-20 bg-white dark:bg-black" />
<button <button
type="button" type="button"
onClick={() => updateSettings()} onClick={() => updateSettings()}
className="inline-flex items-center justify-center w-20 rounded-md shadow h-7 bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium" className="relative z-10 inline-flex items-center justify-center w-20 rounded-md shadow h-8 bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium"
> >
{isPending ? <Spinner className="size-4" /> : "Update"} {isPending ? <Spinner className="size-4" /> : "Update"}
</button> </button>