From 111ab3b082649702e582e39e09bdf3e619057981 Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 25 Feb 2025 15:22:24 +0700 Subject: [PATCH] chore: internal changes --- Cargo.lock | 1 + crates/account/src/registry.rs | 125 +++++++----- crates/app/src/main.rs | 38 ++-- crates/app/src/views/app.rs | 44 +---- crates/app/src/views/chat.rs | 63 ++----- crates/app/src/views/profile.rs | 4 +- crates/app/src/views/relays.rs | 44 ++--- crates/app/src/views/sidebar/compose.rs | 240 +++++++++--------------- crates/chats/Cargo.toml | 1 + crates/chats/src/room.rs | 31 +++ crates/common/src/constants.rs | 7 + 11 files changed, 275 insertions(+), 323 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8be2cdf..7bc5dce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -943,6 +943,7 @@ dependencies = [ "common", "gpui", "itertools 0.13.0", + "log", "nostr-sdk", "oneshot", "smallvec", diff --git a/crates/account/src/registry.rs b/crates/account/src/registry.rs index 24fd0a5..11406f3 100644 --- a/crates/account/src/registry.rs +++ b/crates/account/src/registry.rs @@ -1,4 +1,4 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Error}; use common::{ constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}, profile::NostrProfile, @@ -29,46 +29,37 @@ impl Account { pub fn login(signer: Arc, cx: &AsyncApp) -> Task> { let client = get_client(); - let (tx, rx) = oneshot::channel::>(); - cx.background_spawn(async move { + let task: Task> = cx.background_spawn(async move { // Update nostr signer _ = client.set_signer(signer).await; + // Verify nostr signer and get public key - let result = async { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let metadata = client - .fetch_metadata(public_key, Duration::from_secs(2)) - .await - .ok() - .unwrap_or_default(); + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let metadata = client + .fetch_metadata(public_key, Duration::from_secs(2)) + .await + .unwrap_or_default(); - Ok::<_, anyhow::Error>(NostrProfile::new(public_key, metadata)) - } - .await; - - tx.send(result.ok()).ok(); - }) - .detach(); + Ok(NostrProfile::new(public_key, metadata)) + }); cx.spawn(|cx| async move { - if let Ok(Some(profile)) = rx.await { - cx.update(|cx| { - let this = cx.new(|cx| { - let this = Account { profile }; - // Run initial sync data for this account - if let Some(task) = this.sync(cx) { - task.detach(); - } - // Return - this - }); + match task.await { + Ok(profile) => { + cx.update(|cx| { + let this = cx.new(|cx| { + let this = Self { profile }; + // Run initial sync data for this account + this.sync(cx); + this + }); - Self::set_global(this, cx) - }) - } else { - Err(anyhow!("Login failed")) + Self::set_global(this, cx) + }) + } + Err(e) => Err(anyhow!("Login failed: {}", e)), } }) } @@ -77,41 +68,81 @@ impl Account { &self.profile } - fn sync(&self, cx: &mut Context) -> Option> { + pub fn verify_inbox_relays(&self, cx: &App) -> Task, Error>> { let client = get_client(); let public_key = self.profile.public_key(); - let task = cx.background_spawn(async move { - // Set the default options for this task + cx.background_spawn(async move { + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + let events = client.database().query(filter).await?; + + if let Some(event) = events.first_owned() { + let relays = event + .tags + .filter_standardized(TagKind::Relay) + .filter_map(|t| match t { + TagStandard::Relay(url) => Some(url.to_string()), + _ => None, + }) + .collect::>(); + + Ok(relays) + } else { + Err(anyhow!("Not found")) + } + }) + } + + fn sync(&self, cx: &mut Context) { + let client = get_client(); + let public_key = self.profile.public_key(); + + cx.background_spawn(async move { let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - // Create a filter to get contact list + // Get contact list let contact_list = Filter::new() .kind(Kind::ContactList) .author(public_key) .limit(1); if let Err(e) = client.subscribe(contact_list, Some(opts)).await { - log::error!("Failed to subscribe to contact list: {}", e); + log::error!("Failed to get contact list: {}", e); + } + + // Create a filter to continuously receive new user's data. + let data = Filter::new() + .kinds(vec![Kind::Metadata, Kind::InboxRelays, Kind::RelayList]) + .author(public_key) + .since(Timestamp::now()); + + if let Err(e) = client.subscribe(data, None).await { + log::error!("Failed to subscribe to user data: {}", e); } // Create a filter for getting all gift wrapped events send to current user - let msg = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); - let id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); - if let Err(e) = client.subscribe_with_id(id, msg.clone(), Some(opts)).await { + if let Err(e) = client + .subscribe_with_id(sub_id, filter.clone(), Some(opts)) + .await + { log::error!("Failed to subscribe to all messages: {}", e); } // Create a filter to continuously receive new messages. - let new_msg = msg.limit(0); - let id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); + let new_filter = filter.limit(0); + let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); - if let Err(e) = client.subscribe_with_id(id, new_msg, None).await { + if let Err(e) = client.subscribe_with_id(sub_id, new_filter, None).await { log::error!("Failed to subscribe to new messages: {}", e); } - }); - - Some(task) + }) + .detach(); } } diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 6fea027..48269bc 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -1,7 +1,7 @@ use asset::Assets; use chats::registry::ChatRegistry; use common::constants::{ - ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, KEYRING_SERVICE, NEW_MESSAGE_SUB_ID, + ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, BOOTSTRAP_RELAYS, KEYRING_SERVICE, NEW_MESSAGE_SUB_ID, }; use futures::{select, FutureExt}; use gpui::{ @@ -12,11 +12,9 @@ use gpui::{ use gpui::{point, SharedString, TitlebarOptions}; #[cfg(target_os = "linux")] use gpui::{WindowBackgroundAppearance, WindowDecorations}; -use log::{error, info}; -use nostr_sdk::SubscriptionId; use nostr_sdk::{ pool::prelude::ReqExitPolicy, Client, Event, Filter, Keys, Kind, PublicKey, RelayMessage, - RelayPoolNotification, SubscribeAutoCloseOptions, + RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, }; use smol::Timer; use state::get_client; @@ -58,11 +56,11 @@ fn main() { // Connect to default relays app.background_executor() .spawn(async { - _ = client.add_relay("wss://relay.damus.io/").await; - _ = client.add_relay("wss://relay.primal.net/").await; - _ = client.add_relay("wss://user.kindpag.es/").await; - _ = client.add_relay("wss://purplepag.es/").await; + for relay in BOOTSTRAP_RELAYS.iter() { + _ = client.add_relay(*relay).await; + } _ = client.add_discovery_relay("wss://relaydiscovery.com").await; + _ = client.add_discovery_relay("wss://user.kindpag.es").await; _ = client.connect().await }) .detach(); @@ -131,12 +129,15 @@ fn main() { if let Err(e) = client.database().save_event(&event).await { - error!("Failed to save event: {}", e); + log::error!("Failed to save event: {}", e); } // Send all pubkeys to the batch if let Err(e) = batch_tx.send(pubkeys).await { - error!("Failed to send pubkeys to batch: {}", e) + log::error!( + "Failed to send pubkeys to batch: {}", + e + ) } // Send this event to the GPUI @@ -144,7 +145,10 @@ fn main() { if let Err(e) = event_tx.send(Signal::Event(event)).await { - error!("Failed to send event to GPUI: {}", e) + log::error!( + "Failed to send event to GPUI: {}", + e + ) } } } @@ -153,6 +157,7 @@ fn main() { Kind::ContactList => { let pubkeys = event.tags.public_keys().copied().collect::>(); + sync_metadata(client, pubkeys).await; } _ => {} @@ -161,7 +166,7 @@ fn main() { RelayMessage::EndOfStoredEvents(subscription_id) => { if all_id == *subscription_id { if let Err(e) = event_tx.send(Signal::Eose).await { - error!("Failed to send eose: {}", e) + log::error!("Failed to send eose: {}", e) }; } } @@ -296,14 +301,17 @@ fn main() { } async fn sync_metadata(client: &Client, buffer: HashSet) { - let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + let opts = SubscribeAutoCloseOptions::default() + .exit_policy(ReqExitPolicy::ExitOnEOSE) + .idle_timeout(Some(Duration::from_secs(2))); + let filter = Filter::new() .authors(buffer.iter().cloned()) .kind(Kind::Metadata) .limit(buffer.len()); if let Err(e) = client.subscribe(filter, Some(opts)).await { - error!("Subscribe error: {e}"); + log::error!("Failed to sync metadata: {e}"); } } @@ -362,6 +370,6 @@ async fn restore_window(is_login: bool, cx: &mut AsyncApp) -> anyhow::Result<()> } fn quit(_: &Quit, cx: &mut App) { - info!("Gracefully quitting the application . . ."); + log::info!("Gracefully quitting the application . . ."); cx.quit(); } diff --git a/crates/app/src/views/app.rs b/crates/app/src/views/app.rs index 631b040..63368a2 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/views/app.rs @@ -4,7 +4,6 @@ use gpui::{ Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled, StyledImage, Window, }; -use nostr_sdk::prelude::*; use serde::Deserialize; use state::get_client; use std::sync::Arc; @@ -96,56 +95,27 @@ impl AppView { } fn verify_user_relays(&self, window: &mut Window, cx: &mut Context) { - let Some(account) = Account::global(cx) else { + let Some(model) = Account::global(cx) else { return; }; - let public_key = account.read(cx).get().public_key(); - let client = get_client(); + let account = model.read(cx); + let task = account.verify_inbox_relays(cx); let window_handle = window.window_handle(); - let (tx, rx) = oneshot::channel::>>(); - - cx.background_spawn(async move { - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - let relays = client - .database() - .query(filter) - .await - .ok() - .and_then(|events| events.first_owned()) - .map(|event| { - event - .tags - .filter_standardized(TagKind::Relay) - .filter_map(|t| match t { - TagStandard::Relay(url) => Some(url.to_string()), - _ => None, - }) - .collect::>() - }); - - _ = tx.send(relays); - }) - .detach(); cx.spawn(|this, mut cx| async move { - if let Ok(Some(relays)) = rx.await { + if let Ok(relays) = task.await { _ = cx.update(|cx| { _ = this.update(cx, |this, cx| { - let relays = cx.new(|_| Some(relays)); - this.relays = relays; + this.relays = cx.new(|_| Some(relays)); cx.notify(); }); }); } else { _ = cx.update_window(window_handle, |_, window, cx| { - this.update(cx, |this: &mut Self, cx| { + _ = this.update(cx, |this: &mut Self, cx| { this.render_setup_relays(window, cx) - }) + }); }); } }) diff --git a/crates/app/src/views/chat.rs b/crates/app/src/views/chat.rs index 4d4eb1e..7efb516 100644 --- a/crates/app/src/views/chat.rs +++ b/crates/app/src/views/chat.rs @@ -29,7 +29,8 @@ use ui::{ v_flex, ContextModal, Icon, IconName, Sizable, StyledExt, }; -const ALERT: &str = +const ALERT: &str = "has not set up Messaging (DM) Relays, so they will NOT receive your messages."; +const DESCRIPTION: &str = "This conversation is private. Only members of this chat can see each other's messages."; pub fn init( @@ -186,55 +187,25 @@ impl Chat { return; }; - let client = get_client(); - let (tx, rx) = oneshot::channel::>(); - - let pubkeys: Vec = model - .read(cx) - .members - .iter() - .map(|m| m.public_key()) - .collect(); - - cx.background_spawn(async move { - let mut result = Vec::new(); - - for pubkey in pubkeys.into_iter() { - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(pubkey) - .limit(1); - - let is_ready = if let Ok(events) = client.database().query(filter).await { - events.first_owned().is_some() - } else { - false - }; - - result.push((pubkey, is_ready)); - } - - _ = tx.send(result); - }) - .detach(); + let room = model.read(cx); + let task = room.verify_inbox_relays(cx); cx.spawn(|this, cx| async move { - if let Ok(result) = rx.await { + if let Ok(result) = task.await { _ = cx.update(|cx| { _ = this.update(cx, |this, cx| { - for item in result.into_iter() { + result.into_iter().for_each(|item| { if !item.1 { - let name = this - .room - .read_with(cx, |this, _| this.name().unwrap_or("Unnamed".into())) - .unwrap_or("Unnamed".into()); - - this.push_system_message( - format!("{} has not set up Messaging (DM) Relays, so they will NOT receive your messages.", name), - cx, - ); + if let Ok(Some(member)) = + this.room.read_with(cx, |this, _| this.member(&item.0)) + { + this.push_system_message( + format!("{} {}", ALERT, member.name()), + cx, + ); + } } - } + }); }); }); } @@ -549,7 +520,7 @@ impl Chat { .group_hover("", |this| this.bg(cx.theme().danger)), ) .child( - img("brand/avatar.png") + img("brand/avatar.jpg") .size_8() .rounded_full() .flex_shrink_0(), @@ -574,7 +545,7 @@ impl Chat { .size_8() .text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)), ) - .child(ALERT), + .child(DESCRIPTION), }) } else { div() diff --git a/crates/app/src/views/profile.rs b/crates/app/src/views/profile.rs index 357c411..e8b205b 100644 --- a/crates/app/src/views/profile.rs +++ b/crates/app/src/views/profile.rs @@ -49,7 +49,7 @@ impl Profile { TextInput::new(window, cx) .text_size(Size::XSmall) .small() - .placeholder("https://example.com/avatar.png") + .placeholder("https://example.com/avatar.jpg") }); let website_input = cx.new(|cx| { @@ -309,7 +309,7 @@ impl Render for Profile { if picture.is_empty() { this.child( - img("brand/avatar.png") + img("brand/avatar.jpg") .size_10() .rounded_full() .flex_shrink_0(), diff --git a/crates/app/src/views/relays.rs b/crates/app/src/views/relays.rs index 7ec9f26..94be776 100644 --- a/crates/app/src/views/relays.rs +++ b/crates/app/src/views/relays.rs @@ -1,6 +1,6 @@ use gpui::{ div, prelude::FluentBuilder, px, uniform_list, AppContext, Context, Entity, FocusHandle, - InteractiveElement, IntoElement, ParentElement, Render, Styled, TextAlign, Window, + InteractiveElement, IntoElement, ParentElement, Render, Styled, Task, TextAlign, Window, }; use nostr_sdk::prelude::*; use state::get_client; @@ -63,31 +63,23 @@ impl Relays { let relays = self.relays.read(cx).clone(); let window_handle = window.window_handle(); + // Show loading spinner self.set_loading(true, cx); - let client = get_client(); - let (tx, rx) = oneshot::channel(); - - cx.background_spawn(async move { - let signer = client.signer().await.expect("Signer is required"); - let public_key = signer - .get_public_key() - .await - .expect("Cannot get public key"); + let task: Task> = cx.background_spawn(async move { + let client = get_client(); + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; // If user didn't have any NIP-65 relays, add default ones - // TODO: Is this really necessary? - if let Ok(relay_list) = client.database().relay_list(public_key).await { - if relay_list.is_empty() { - let builder = EventBuilder::relay_list(vec![ - (RelayUrl::parse("wss://relay.damus.io/").unwrap(), None), - (RelayUrl::parse("wss://relay.primal.net/").unwrap(), None), - (RelayUrl::parse("wss://nos.lol/").unwrap(), None), - ]); + if client.database().relay_list(public_key).await?.is_empty() { + let builder = EventBuilder::relay_list(vec![ + (RelayUrl::parse("wss://relay.damus.io/").unwrap(), None), + (RelayUrl::parse("wss://relay.primal.net/").unwrap(), None), + ]); - if let Err(e) = client.send_event_builder(builder).await { - log::error!("Failed to send relay list event: {}", e) - } + if let Err(e) = client.send_event_builder(builder).await { + log::error!("Failed to send relay list event: {}", e); } } @@ -97,15 +89,13 @@ impl Relays { .collect(); let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); + let output = client.send_event_builder(builder).await?; - if let Ok(output) = client.send_event_builder(builder).await { - _ = tx.send(output.val); - }; - }) - .detach(); + Ok(output.val) + }); cx.spawn(|this, mut cx| async move { - if rx.await.is_ok() { + if task.await.is_ok() { _ = cx.update_window(window_handle, |_, window, cx| { _ = this.update(cx, |this, cx| { this.set_loading(false, cx); diff --git a/crates/app/src/views/sidebar/compose.rs b/crates/app/src/views/sidebar/compose.rs index cbf1185..744075a 100644 --- a/crates/app/src/views/sidebar/compose.rs +++ b/crates/app/src/views/sidebar/compose.rs @@ -3,10 +3,12 @@ use common::{profile::NostrProfile, utils::random_name}; use gpui::{ div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, - Render, SharedString, StatefulInteractiveElement, Styled, Subscription, TextAlign, Window, + Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, + Window, }; use nostr_sdk::prelude::*; use serde::Deserialize; +use smallvec::{smallvec, SmallVec}; use smol::Timer; use state::get_client; use std::{collections::HashSet, time::Duration}; @@ -17,7 +19,7 @@ use ui::{ ContextModal, Icon, IconName, Sizable, Size, StyledExt, }; -const ALERT: &str = +const DESCRIPTION: &str = "Start a conversation with someone using their npub or NIP-05 (like foo@bar.com)."; #[derive(Clone, PartialEq, Eq, Deserialize)] @@ -35,7 +37,7 @@ pub struct Compose { is_submitting: bool, error_message: Entity>, #[allow(dead_code)] - subscriptions: Vec, + subscriptions: SmallVec<[Subscription; 1]>, } impl Compose { @@ -43,7 +45,6 @@ impl Compose { let contacts = cx.new(|_| Vec::new()); let selected = cx.new(|_| HashSet::new()); let error_message = cx.new(|_| None); - let mut subscriptions = Vec::new(); let title_input = cx.new(|cx| { let name = random_name(2); @@ -63,17 +64,15 @@ impl Compose { .placeholder("npub1...") }); + let mut subscriptions = smallvec![]; + // Handle Enter event for user input subscriptions.push(cx.subscribe_in( &user_input, window, - move |this, input, input_event, window, cx| { + move |this, _, input_event, window, cx| { if let InputEvent::PressEnter = input_event { - if input.read(cx).text().contains("@") { - this.add_nip05(window, cx); - } else { - this.add_npub(window, cx) - } + this.add(window, cx); } }, )); @@ -147,50 +146,48 @@ impl Compose { } let tags = Tags::from_list(tag_list); - let client = get_client(); let window_handle = window.window_handle(); - let (tx, rx) = oneshot::channel::(); - cx.background_spawn(async move { - let signer = client.signer().await.expect("Signer is required"); + let event: Task> = cx.background_spawn(async move { + let client = get_client(); + let signer = client.signer().await?; // [IMPORTANT] // Make sure this event is never send, // this event existed just use for convert to Coop's Chat Room later. - if let Ok(event) = EventBuilder::private_msg_rumor(*pubkeys.last().unwrap(), "") + let event = EventBuilder::private_msg_rumor(*pubkeys.last().unwrap(), "") .tags(tags) .sign(&signer) - .await - { - _ = tx.send(event) - }; - }) - .detach(); + .await?; + + Ok(event) + }); cx.spawn(|this, mut cx| async move { - if let Ok(event) = rx.await { + if let Ok(event) = event.await { _ = cx.update_window(window_handle, |_, window, cx| { // Stop loading spinner _ = this.update(cx, |this, cx| { this.set_submitting(false, cx); }); - if let Some(chats) = ChatRegistry::global(cx) { - let room = Room::new(&event, cx); + let Some(chats) = ChatRegistry::global(cx) else { + return; + }; + let room = Room::new(&event, cx); - chats.update(cx, |state, cx| { - match state.push_room(room, cx) { - Ok(_) => { - // TODO: open chat panel - window.close_modal(cx); - } - Err(e) => { - _ = this.update(cx, |this, cx| { - this.set_error(Some(e.to_string().into()), cx); - }); - } + chats.update(cx, |state, cx| { + match state.push_room(room, cx) { + Ok(_) => { + // TODO: automatically open newly created chat panel + window.close_modal(cx); } - }); - } + Err(e) => { + _ = this.update(cx, |this, cx| { + this.set_error(Some(e.to_string().into()), cx); + }); + } + } + }); }); } }) @@ -209,128 +206,77 @@ impl Compose { self.is_submitting } - fn add_npub(&mut self, window: &mut Window, cx: &mut Context) { + fn add(&mut self, window: &mut Window, cx: &mut Context) { + let client = get_client(); let window_handle = window.window_handle(); let content = self.user_input.read(cx).text().to_string(); // Show loading spinner self.set_loading(true, cx); - let Ok(public_key) = PublicKey::parse(&content) else { - self.set_loading(false, cx); - self.set_error(Some("Public Key is not valid".into()), cx); - return; + let task: Task> = if content.starts_with("npub1") { + let Ok(public_key) = PublicKey::parse(&content) else { + self.set_loading(false, cx); + self.set_error(Some("Public Key is not valid".into()), cx); + return; + }; + + cx.background_spawn(async move { + let metadata = client + .fetch_metadata(public_key, Duration::from_secs(2)) + .await?; + + Ok(NostrProfile::new(public_key, metadata)) + }) + } else { + cx.background_spawn(async move { + let profile = nip05::profile(&content, None).await?; + let public_key = profile.public_key; + + let metadata = client + .fetch_metadata(public_key, Duration::from_secs(2)) + .await?; + + Ok(NostrProfile::new(public_key, metadata)) + }) }; - if self - .contacts - .read(cx) - .iter() - .any(|c| c.public_key() == public_key) - { - self.set_loading(false, cx); - return; - }; - - let client = get_client(); - let (tx, rx) = oneshot::channel::(); - - cx.background_spawn(async move { - let metadata = (client - .fetch_metadata(public_key, Duration::from_secs(2)) - .await) - .unwrap_or_default(); - - _ = tx.send(metadata); - }) - .detach(); - cx.spawn(|this, mut cx| async move { - if let Ok(metadata) = rx.await { - _ = cx.update_window(window_handle, |_, window, cx| { - _ = this.update(cx, |this, cx| { - this.contacts.update(cx, |this, cx| { - this.insert(0, NostrProfile::new(public_key, metadata)); - cx.notify(); - }); + match task.await { + Ok(profile) => { + _ = cx.update_window(window_handle, |_, window, cx| { + _ = this.update(cx, |this, cx| { + let public_key = profile.public_key(); - this.selected.update(cx, |this, cx| { - this.insert(public_key); - cx.notify(); - }); + this.contacts.update(cx, |this, cx| { + this.insert(0, profile); + cx.notify(); + }); - // Stop loading indicator - this.set_loading(false, cx); + this.selected.update(cx, |this, cx| { + this.insert(public_key); + cx.notify(); + }); - // Clear input - this.user_input.update(cx, |this, cx| { - this.set_text("", window, cx); - cx.notify(); + // Stop loading indicator + this.set_loading(false, cx); + + // Clear input + this.user_input.update(cx, |this, cx| { + this.set_text("", window, cx); + cx.notify(); + }); }); }); - }); - } - }) - .detach(); - } - - fn add_nip05(&mut self, window: &mut Window, cx: &mut Context) { - let window_handle = window.window_handle(); - let content = self.user_input.read(cx).text().to_string(); - - // Show loading spinner - self.set_loading(true, cx); - - let client = get_client(); - let (tx, rx) = oneshot::channel::>(); - - cx.background_spawn(async move { - if let Ok(profile) = nip05::profile(&content, None).await { - let metadata = (client - .fetch_metadata(profile.public_key, Duration::from_secs(2)) - .await) - .unwrap_or_default(); - - _ = tx.send(Some(NostrProfile::new(profile.public_key, metadata))); - } else { - _ = tx.send(None); - } - }) - .detach(); - - cx.spawn(|this, mut cx| async move { - if let Ok(Some(profile)) = rx.await { - _ = cx.update_window(window_handle, |_, window, cx| { - _ = this.update(cx, |this, cx| { - let public_key = profile.public_key(); - - this.contacts.update(cx, |this, cx| { - this.insert(0, profile); - cx.notify(); - }); - - this.selected.update(cx, |this, cx| { - this.insert(public_key); - cx.notify(); - }); - - // Stop loading indicator - this.set_loading(false, cx); - - // Clear input - this.user_input.update(cx, |this, cx| { - this.set_text("", window, cx); - cx.notify(); + } + Err(e) => { + _ = cx.update_window(window_handle, |_, _, cx| { + _ = this.update(cx, |this, cx| { + this.set_loading(false, cx); + this.set_error(Some(e.to_string().into()), cx); }); }); - }); - } else { - _ = cx.update_window(window_handle, |_, _, cx| { - _ = this.update(cx, |this, cx| { - this.set_loading(false, cx); - this.set_error(Some("NIP-05 Address is not valid".into()), cx); - }); - }); + } } }) .detach(); @@ -395,7 +341,7 @@ impl Render for Compose { .px_2() .text_xs() .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) - .child(ALERT), + .child(DESCRIPTION), ) .when_some(self.error_message.read(cx).as_ref(), |this, msg| { this.child( @@ -439,11 +385,7 @@ impl Render for Compose { .rounded(ButtonRounded::Size(px(9999.))) .loading(self.is_loading) .on_click(cx.listener(|this, _, window, cx| { - if this.user_input.read(cx).text().contains("@") { - this.add_nip05(window, cx); - } else { - this.add_npub(window, cx); - } + this.add(window, cx); })), ) .child(self.user_input.clone()), diff --git a/crates/chats/Cargo.toml b/crates/chats/Cargo.toml index 9e5cc19..fc84850 100644 --- a/crates/chats/Cargo.toml +++ b/crates/chats/Cargo.toml @@ -16,3 +16,4 @@ chrono.workspace = true smallvec.workspace = true smol.workspace = true oneshot.workspace = true +log.workspace = true diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs index ac12aee..86b64e5 100644 --- a/crates/chats/src/room.rs +++ b/crates/chats/src/room.rs @@ -128,6 +128,35 @@ impl Room { self.last_seen.ago() } + /// Sync inbox relays for all room's members + pub fn verify_inbox_relays(&self, cx: &App) -> Task, Error>> { + let client = get_client(); + let pubkeys = self.public_keys(); + + cx.background_spawn(async move { + let mut result = Vec::with_capacity(pubkeys.len()); + + for pubkey in pubkeys.into_iter() { + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(pubkey) + .limit(1); + + let is_ready = client + .database() + .query(filter) + .await + .ok() + .and_then(|events| events.first_owned()) + .is_some(); + + result.push((pubkey, is_ready)); + } + + Ok(result) + }) + } + /// Send message to all room's members pub fn send_message(&self, content: String, cx: &App) -> Task, Error>> { let client = get_client(); @@ -155,6 +184,8 @@ impl Room { .send_private_msg(*pubkey, &content, tags.clone()) .await { + log::error!("Failed to send message to {}: {}", pubkey.to_bech32()?, e); + // Convert error into string msg.push(e.to_string()); } } diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index 6135411..64ab475 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -2,6 +2,13 @@ pub const KEYRING_SERVICE: &str = "Coop Safe Storage"; pub const APP_NAME: &str = "Coop"; pub const APP_ID: &str = "su.reya.coop"; +/// Bootstrap relays +pub const BOOTSTRAP_RELAYS: [&str; 3] = [ + "wss://relay.damus.io", + "wss://relay.primal.net", + "wss://purplepag.es", +]; + /// Subscriptions pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwraps"; pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps";