chore: internal changes
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user