chore: internal changes

This commit is contained in:
2025-02-25 15:22:24 +07:00
parent 1c4806bd92
commit 111ab3b082
11 changed files with 275 additions and 323 deletions

1
Cargo.lock generated
View File

@@ -943,6 +943,7 @@ dependencies = [
"common", "common",
"gpui", "gpui",
"itertools 0.13.0", "itertools 0.13.0",
"log",
"nostr-sdk", "nostr-sdk",
"oneshot", "oneshot",
"smallvec", "smallvec",

View File

@@ -1,4 +1,4 @@
use anyhow::anyhow; use anyhow::{anyhow, Error};
use common::{ use common::{
constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}, constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID},
profile::NostrProfile, profile::NostrProfile,
@@ -29,46 +29,37 @@ impl Account {
pub fn login(signer: Arc<dyn NostrSigner>, cx: &AsyncApp) -> Task<Result<(), anyhow::Error>> { pub fn login(signer: Arc<dyn NostrSigner>, cx: &AsyncApp) -> Task<Result<(), anyhow::Error>> {
let client = get_client(); let client = get_client();
let (tx, rx) = oneshot::channel::<Option<NostrProfile>>();
cx.background_spawn(async move { let task: Task<Result<NostrProfile, anyhow::Error>> = cx.background_spawn(async move {
// Update nostr signer // Update nostr signer
_ = client.set_signer(signer).await; _ = client.set_signer(signer).await;
// Verify nostr signer and get public key // Verify nostr signer and get public key
let result = async { let signer = client.signer().await?;
let signer = client.signer().await?; let public_key = signer.get_public_key().await?;
let public_key = signer.get_public_key().await?; let metadata = client
let metadata = client .fetch_metadata(public_key, Duration::from_secs(2))
.fetch_metadata(public_key, Duration::from_secs(2)) .await
.await .unwrap_or_default();
.ok()
.unwrap_or_default();
Ok::<_, anyhow::Error>(NostrProfile::new(public_key, metadata)) Ok(NostrProfile::new(public_key, metadata))
} });
.await;
tx.send(result.ok()).ok();
})
.detach();
cx.spawn(|cx| async move { cx.spawn(|cx| async move {
if let Ok(Some(profile)) = rx.await { match task.await {
cx.update(|cx| { Ok(profile) => {
let this = cx.new(|cx| { cx.update(|cx| {
let this = Account { profile }; let this = cx.new(|cx| {
// Run initial sync data for this account let this = Self { profile };
if let Some(task) = this.sync(cx) { // Run initial sync data for this account
task.detach(); this.sync(cx);
} this
// Return });
this
});
Self::set_global(this, cx) Self::set_global(this, cx)
}) })
} else { }
Err(anyhow!("Login failed")) Err(e) => Err(anyhow!("Login failed: {}", e)),
} }
}) })
} }
@@ -77,41 +68,81 @@ impl Account {
&self.profile &self.profile
} }
fn sync(&self, cx: &mut Context<Self>) -> Option<Task<()>> { pub fn verify_inbox_relays(&self, cx: &App) -> Task<Result<Vec<String>, Error>> {
let client = get_client(); let client = get_client();
let public_key = self.profile.public_key(); let public_key = self.profile.public_key();
let task = cx.background_spawn(async move { cx.background_spawn(async move {
// Set the default options for this task 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::<Vec<_>>();
Ok(relays)
} else {
Err(anyhow!("Not found"))
}
})
}
fn sync(&self, cx: &mut Context<Self>) {
let client = get_client();
let public_key = self.profile.public_key();
cx.background_spawn(async move {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
// Create a filter to get contact list // Get contact list
let contact_list = Filter::new() let contact_list = Filter::new()
.kind(Kind::ContactList) .kind(Kind::ContactList)
.author(public_key) .author(public_key)
.limit(1); .limit(1);
if let Err(e) = client.subscribe(contact_list, Some(opts)).await { 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 // Create a filter for getting all gift wrapped events send to current user
let msg = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); 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); log::error!("Failed to subscribe to all messages: {}", e);
} }
// Create a filter to continuously receive new messages. // Create a filter to continuously receive new messages.
let new_msg = msg.limit(0); let new_filter = filter.limit(0);
let id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); 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); log::error!("Failed to subscribe to new messages: {}", e);
} }
}); })
.detach();
Some(task)
} }
} }

View File

@@ -1,7 +1,7 @@
use asset::Assets; use asset::Assets;
use chats::registry::ChatRegistry; use chats::registry::ChatRegistry;
use common::constants::{ 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 futures::{select, FutureExt};
use gpui::{ use gpui::{
@@ -12,11 +12,9 @@ use gpui::{
use gpui::{point, SharedString, TitlebarOptions}; use gpui::{point, SharedString, TitlebarOptions};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use gpui::{WindowBackgroundAppearance, WindowDecorations}; use gpui::{WindowBackgroundAppearance, WindowDecorations};
use log::{error, info};
use nostr_sdk::SubscriptionId;
use nostr_sdk::{ use nostr_sdk::{
pool::prelude::ReqExitPolicy, Client, Event, Filter, Keys, Kind, PublicKey, RelayMessage, pool::prelude::ReqExitPolicy, Client, Event, Filter, Keys, Kind, PublicKey, RelayMessage,
RelayPoolNotification, SubscribeAutoCloseOptions, RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId,
}; };
use smol::Timer; use smol::Timer;
use state::get_client; use state::get_client;
@@ -58,11 +56,11 @@ fn main() {
// Connect to default relays // Connect to default relays
app.background_executor() app.background_executor()
.spawn(async { .spawn(async {
_ = client.add_relay("wss://relay.damus.io/").await; for relay in BOOTSTRAP_RELAYS.iter() {
_ = client.add_relay("wss://relay.primal.net/").await; _ = client.add_relay(*relay).await;
_ = client.add_relay("wss://user.kindpag.es/").await; }
_ = client.add_relay("wss://purplepag.es/").await;
_ = client.add_discovery_relay("wss://relaydiscovery.com").await; _ = client.add_discovery_relay("wss://relaydiscovery.com").await;
_ = client.add_discovery_relay("wss://user.kindpag.es").await;
_ = client.connect().await _ = client.connect().await
}) })
.detach(); .detach();
@@ -131,12 +129,15 @@ fn main() {
if let Err(e) = if let Err(e) =
client.database().save_event(&event).await 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 // Send all pubkeys to the batch
if let Err(e) = batch_tx.send(pubkeys).await { 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 // Send this event to the GPUI
@@ -144,7 +145,10 @@ fn main() {
if let Err(e) = if let Err(e) =
event_tx.send(Signal::Event(event)).await 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 => { Kind::ContactList => {
let pubkeys = let pubkeys =
event.tags.public_keys().copied().collect::<HashSet<_>>(); event.tags.public_keys().copied().collect::<HashSet<_>>();
sync_metadata(client, pubkeys).await; sync_metadata(client, pubkeys).await;
} }
_ => {} _ => {}
@@ -161,7 +166,7 @@ fn main() {
RelayMessage::EndOfStoredEvents(subscription_id) => { RelayMessage::EndOfStoredEvents(subscription_id) => {
if all_id == *subscription_id { if all_id == *subscription_id {
if let Err(e) = event_tx.send(Signal::Eose).await { 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<PublicKey>) { async fn sync_metadata(client: &Client, buffer: HashSet<PublicKey>) {
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() let filter = Filter::new()
.authors(buffer.iter().cloned()) .authors(buffer.iter().cloned())
.kind(Kind::Metadata) .kind(Kind::Metadata)
.limit(buffer.len()); .limit(buffer.len());
if let Err(e) = client.subscribe(filter, Some(opts)).await { 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) { fn quit(_: &Quit, cx: &mut App) {
info!("Gracefully quitting the application . . ."); log::info!("Gracefully quitting the application . . .");
cx.quit(); cx.quit();
} }

View File

@@ -4,7 +4,6 @@ use gpui::{
Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled, Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled,
StyledImage, Window, StyledImage, Window,
}; };
use nostr_sdk::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use state::get_client; use state::get_client;
use std::sync::Arc; use std::sync::Arc;
@@ -96,56 +95,27 @@ impl AppView {
} }
fn verify_user_relays(&self, window: &mut Window, cx: &mut Context<Self>) { fn verify_user_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
let Some(account) = Account::global(cx) else { let Some(model) = Account::global(cx) else {
return; return;
}; };
let public_key = account.read(cx).get().public_key(); let account = model.read(cx);
let client = get_client(); let task = account.verify_inbox_relays(cx);
let window_handle = window.window_handle(); let window_handle = window.window_handle();
let (tx, rx) = oneshot::channel::<Option<Vec<String>>>();
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::<Vec<_>>()
});
_ = tx.send(relays);
})
.detach();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
if let Ok(Some(relays)) = rx.await { if let Ok(relays) = task.await {
_ = cx.update(|cx| { _ = cx.update(|cx| {
_ = this.update(cx, |this, cx| { _ = this.update(cx, |this, cx| {
let relays = cx.new(|_| Some(relays)); this.relays = cx.new(|_| Some(relays));
this.relays = relays;
cx.notify(); cx.notify();
}); });
}); });
} else { } else {
_ = cx.update_window(window_handle, |_, window, cx| { _ = 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) this.render_setup_relays(window, cx)
}) });
}); });
} }
}) })

View File

@@ -29,7 +29,8 @@ use ui::{
v_flex, ContextModal, Icon, IconName, Sizable, StyledExt, 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."; "This conversation is private. Only members of this chat can see each other's messages.";
pub fn init( pub fn init(
@@ -186,55 +187,25 @@ impl Chat {
return; return;
}; };
let client = get_client(); let room = model.read(cx);
let (tx, rx) = oneshot::channel::<Vec<(PublicKey, bool)>>(); let task = room.verify_inbox_relays(cx);
let pubkeys: Vec<PublicKey> = 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();
cx.spawn(|this, cx| async move { cx.spawn(|this, cx| async move {
if let Ok(result) = rx.await { if let Ok(result) = task.await {
_ = cx.update(|cx| { _ = cx.update(|cx| {
_ = this.update(cx, |this, cx| { _ = this.update(cx, |this, cx| {
for item in result.into_iter() { result.into_iter().for_each(|item| {
if !item.1 { if !item.1 {
let name = this if let Ok(Some(member)) =
.room this.room.read_with(cx, |this, _| this.member(&item.0))
.read_with(cx, |this, _| this.name().unwrap_or("Unnamed".into())) {
.unwrap_or("Unnamed".into()); this.push_system_message(
format!("{} {}", ALERT, member.name()),
this.push_system_message( cx,
format!("{} has not set up Messaging (DM) Relays, so they will NOT receive your messages.", name), );
cx, }
);
} }
} });
}); });
}); });
} }
@@ -549,7 +520,7 @@ impl Chat {
.group_hover("", |this| this.bg(cx.theme().danger)), .group_hover("", |this| this.bg(cx.theme().danger)),
) )
.child( .child(
img("brand/avatar.png") img("brand/avatar.jpg")
.size_8() .size_8()
.rounded_full() .rounded_full()
.flex_shrink_0(), .flex_shrink_0(),
@@ -574,7 +545,7 @@ impl Chat {
.size_8() .size_8()
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)), .text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
) )
.child(ALERT), .child(DESCRIPTION),
}) })
} else { } else {
div() div()

View File

@@ -49,7 +49,7 @@ impl Profile {
TextInput::new(window, cx) TextInput::new(window, cx)
.text_size(Size::XSmall) .text_size(Size::XSmall)
.small() .small()
.placeholder("https://example.com/avatar.png") .placeholder("https://example.com/avatar.jpg")
}); });
let website_input = cx.new(|cx| { let website_input = cx.new(|cx| {
@@ -309,7 +309,7 @@ impl Render for Profile {
if picture.is_empty() { if picture.is_empty() {
this.child( this.child(
img("brand/avatar.png") img("brand/avatar.jpg")
.size_10() .size_10()
.rounded_full() .rounded_full()
.flex_shrink_0(), .flex_shrink_0(),

View File

@@ -1,6 +1,6 @@
use gpui::{ use gpui::{
div, prelude::FluentBuilder, px, uniform_list, AppContext, Context, Entity, FocusHandle, 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 nostr_sdk::prelude::*;
use state::get_client; use state::get_client;
@@ -63,31 +63,23 @@ impl Relays {
let relays = self.relays.read(cx).clone(); let relays = self.relays.read(cx).clone();
let window_handle = window.window_handle(); let window_handle = window.window_handle();
// Show loading spinner
self.set_loading(true, cx); self.set_loading(true, cx);
let client = get_client(); let task: Task<Result<EventId, anyhow::Error>> = cx.background_spawn(async move {
let (tx, rx) = oneshot::channel(); let client = get_client();
let signer = client.signer().await?;
cx.background_spawn(async move { let public_key = signer.get_public_key().await?;
let signer = client.signer().await.expect("Signer is required");
let public_key = signer
.get_public_key()
.await
.expect("Cannot get public key");
// If user didn't have any NIP-65 relays, add default ones // If user didn't have any NIP-65 relays, add default ones
// TODO: Is this really necessary? if client.database().relay_list(public_key).await?.is_empty() {
if let Ok(relay_list) = client.database().relay_list(public_key).await { let builder = EventBuilder::relay_list(vec![
if relay_list.is_empty() { (RelayUrl::parse("wss://relay.damus.io/").unwrap(), None),
let builder = EventBuilder::relay_list(vec![ (RelayUrl::parse("wss://relay.primal.net/").unwrap(), None),
(RelayUrl::parse("wss://relay.damus.io/").unwrap(), None), ]);
(RelayUrl::parse("wss://relay.primal.net/").unwrap(), None),
(RelayUrl::parse("wss://nos.lol/").unwrap(), None),
]);
if let Err(e) = client.send_event_builder(builder).await { if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send relay list event: {}", e) log::error!("Failed to send relay list event: {}", e);
}
} }
} }
@@ -97,15 +89,13 @@ impl Relays {
.collect(); .collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); 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 { Ok(output.val)
_ = tx.send(output.val); });
};
})
.detach();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
if rx.await.is_ok() { if task.await.is_ok() {
_ = cx.update_window(window_handle, |_, window, cx| { _ = cx.update_window(window_handle, |_, window, cx| {
_ = this.update(cx, |this, cx| { _ = this.update(cx, |this, cx| {
this.set_loading(false, cx); this.set_loading(false, cx);

View File

@@ -3,10 +3,12 @@ use common::{profile::NostrProfile, utils::random_name};
use gpui::{ use gpui::{
div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App, div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App,
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, 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 nostr_sdk::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::Timer; use smol::Timer;
use state::get_client; use state::get_client;
use std::{collections::HashSet, time::Duration}; use std::{collections::HashSet, time::Duration};
@@ -17,7 +19,7 @@ use ui::{
ContextModal, Icon, IconName, Sizable, Size, StyledExt, 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)."; "Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
#[derive(Clone, PartialEq, Eq, Deserialize)] #[derive(Clone, PartialEq, Eq, Deserialize)]
@@ -35,7 +37,7 @@ pub struct Compose {
is_submitting: bool, is_submitting: bool,
error_message: Entity<Option<SharedString>>, error_message: Entity<Option<SharedString>>,
#[allow(dead_code)] #[allow(dead_code)]
subscriptions: Vec<Subscription>, subscriptions: SmallVec<[Subscription; 1]>,
} }
impl Compose { impl Compose {
@@ -43,7 +45,6 @@ impl Compose {
let contacts = cx.new(|_| Vec::new()); let contacts = cx.new(|_| Vec::new());
let selected = cx.new(|_| HashSet::new()); let selected = cx.new(|_| HashSet::new());
let error_message = cx.new(|_| None); let error_message = cx.new(|_| None);
let mut subscriptions = Vec::new();
let title_input = cx.new(|cx| { let title_input = cx.new(|cx| {
let name = random_name(2); let name = random_name(2);
@@ -63,17 +64,15 @@ impl Compose {
.placeholder("npub1...") .placeholder("npub1...")
}); });
let mut subscriptions = smallvec![];
// Handle Enter event for user input // Handle Enter event for user input
subscriptions.push(cx.subscribe_in( subscriptions.push(cx.subscribe_in(
&user_input, &user_input,
window, window,
move |this, input, input_event, window, cx| { move |this, _, input_event, window, cx| {
if let InputEvent::PressEnter = input_event { if let InputEvent::PressEnter = input_event {
if input.read(cx).text().contains("@") { this.add(window, cx);
this.add_nip05(window, cx);
} else {
this.add_npub(window, cx)
}
} }
}, },
)); ));
@@ -147,50 +146,48 @@ impl Compose {
} }
let tags = Tags::from_list(tag_list); let tags = Tags::from_list(tag_list);
let client = get_client();
let window_handle = window.window_handle(); let window_handle = window.window_handle();
let (tx, rx) = oneshot::channel::<Event>();
cx.background_spawn(async move { let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move {
let signer = client.signer().await.expect("Signer is required"); let client = get_client();
let signer = client.signer().await?;
// [IMPORTANT] // [IMPORTANT]
// Make sure this event is never send, // Make sure this event is never send,
// this event existed just use for convert to Coop's Chat Room later. // 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) .tags(tags)
.sign(&signer) .sign(&signer)
.await .await?;
{
_ = tx.send(event) Ok(event)
}; });
})
.detach();
cx.spawn(|this, mut cx| async move { 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| { _ = cx.update_window(window_handle, |_, window, cx| {
// Stop loading spinner // Stop loading spinner
_ = this.update(cx, |this, cx| { _ = this.update(cx, |this, cx| {
this.set_submitting(false, cx); this.set_submitting(false, cx);
}); });
if let Some(chats) = ChatRegistry::global(cx) { let Some(chats) = ChatRegistry::global(cx) else {
let room = Room::new(&event, cx); return;
};
let room = Room::new(&event, cx);
chats.update(cx, |state, cx| { chats.update(cx, |state, cx| {
match state.push_room(room, cx) { match state.push_room(room, cx) {
Ok(_) => { Ok(_) => {
// TODO: open chat panel // TODO: automatically open newly created chat panel
window.close_modal(cx); window.close_modal(cx);
}
Err(e) => {
_ = this.update(cx, |this, cx| {
this.set_error(Some(e.to_string().into()), 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 self.is_submitting
} }
fn add_npub(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let window_handle = window.window_handle(); let window_handle = window.window_handle();
let content = self.user_input.read(cx).text().to_string(); let content = self.user_input.read(cx).text().to_string();
// Show loading spinner // Show loading spinner
self.set_loading(true, cx); self.set_loading(true, cx);
let Ok(public_key) = PublicKey::parse(&content) else { let task: Task<Result<NostrProfile, anyhow::Error>> = if content.starts_with("npub1") {
self.set_loading(false, cx); let Ok(public_key) = PublicKey::parse(&content) else {
self.set_error(Some("Public Key is not valid".into()), cx); self.set_loading(false, cx);
return; 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::<Metadata>();
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 { cx.spawn(|this, mut cx| async move {
if let Ok(metadata) = rx.await { match task.await {
_ = cx.update_window(window_handle, |_, window, cx| { Ok(profile) => {
_ = this.update(cx, |this, cx| { _ = cx.update_window(window_handle, |_, window, cx| {
this.contacts.update(cx, |this, cx| { _ = this.update(cx, |this, cx| {
this.insert(0, NostrProfile::new(public_key, metadata)); let public_key = profile.public_key();
cx.notify();
});
this.selected.update(cx, |this, cx| { this.contacts.update(cx, |this, cx| {
this.insert(public_key); this.insert(0, profile);
cx.notify(); cx.notify();
}); });
// Stop loading indicator this.selected.update(cx, |this, cx| {
this.set_loading(false, cx); this.insert(public_key);
cx.notify();
});
// Clear input // Stop loading indicator
this.user_input.update(cx, |this, cx| { this.set_loading(false, cx);
this.set_text("", window, cx);
cx.notify(); // Clear input
this.user_input.update(cx, |this, cx| {
this.set_text("", window, cx);
cx.notify();
});
}); });
}); });
}); }
} Err(e) => {
}) _ = cx.update_window(window_handle, |_, _, cx| {
.detach(); _ = this.update(cx, |this, cx| {
} this.set_loading(false, cx);
this.set_error(Some(e.to_string().into()), cx);
fn add_nip05(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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::<Option<NostrProfile>>();
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();
}); });
}); });
}); }
} 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(); .detach();
@@ -395,7 +341,7 @@ impl Render for Compose {
.px_2() .px_2()
.text_xs() .text_xs()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(ALERT), .child(DESCRIPTION),
) )
.when_some(self.error_message.read(cx).as_ref(), |this, msg| { .when_some(self.error_message.read(cx).as_ref(), |this, msg| {
this.child( this.child(
@@ -439,11 +385,7 @@ impl Render for Compose {
.rounded(ButtonRounded::Size(px(9999.))) .rounded(ButtonRounded::Size(px(9999.)))
.loading(self.is_loading) .loading(self.is_loading)
.on_click(cx.listener(|this, _, window, cx| { .on_click(cx.listener(|this, _, window, cx| {
if this.user_input.read(cx).text().contains("@") { this.add(window, cx);
this.add_nip05(window, cx);
} else {
this.add_npub(window, cx);
}
})), })),
) )
.child(self.user_input.clone()), .child(self.user_input.clone()),

View File

@@ -16,3 +16,4 @@ chrono.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
oneshot.workspace = true oneshot.workspace = true
log.workspace = true

View File

@@ -128,6 +128,35 @@ impl Room {
self.last_seen.ago() self.last_seen.ago()
} }
/// Sync inbox relays for all room's members
pub fn verify_inbox_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, 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 /// Send message to all room's members
pub fn send_message(&self, content: String, cx: &App) -> Task<Result<Vec<String>, Error>> { pub fn send_message(&self, content: String, cx: &App) -> Task<Result<Vec<String>, Error>> {
let client = get_client(); let client = get_client();
@@ -155,6 +184,8 @@ impl Room {
.send_private_msg(*pubkey, &content, tags.clone()) .send_private_msg(*pubkey, &content, tags.clone())
.await .await
{ {
log::error!("Failed to send message to {}: {}", pubkey.to_bech32()?, e);
// Convert error into string
msg.push(e.to_string()); msg.push(e.to_string());
} }
} }

View File

@@ -2,6 +2,13 @@ pub const KEYRING_SERVICE: &str = "Coop Safe Storage";
pub const APP_NAME: &str = "Coop"; pub const APP_NAME: &str = "Coop";
pub const APP_ID: &str = "su.reya.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 /// Subscriptions
pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwraps"; pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwraps";
pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps"; pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps";