feat: verify dm relays when open chat panel
This commit is contained in:
@@ -8,8 +8,8 @@ use common::{
|
||||
utils::{compare, nip96_upload},
|
||||
};
|
||||
use gpui::{
|
||||
div, img, list, prelude::FluentBuilder, px, white, AnyElement, App, AppContext, Context,
|
||||
Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement,
|
||||
div, img, list, prelude::FluentBuilder, px, relative, svg, white, AnyElement, App, AppContext,
|
||||
Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, WeakEntity,
|
||||
Window,
|
||||
@@ -24,11 +24,15 @@ use ui::{
|
||||
button::{Button, ButtonRounded, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::{InputEvent, TextInput},
|
||||
notification::Notification,
|
||||
popup_menu::PopupMenu,
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
v_flex, ContextModal, Icon, IconName, Sizable, StyledExt,
|
||||
};
|
||||
|
||||
const ALERT: &str =
|
||||
"This conversation is private. Only members of this chat can see each other's messages.";
|
||||
|
||||
pub fn init(
|
||||
id: &u64,
|
||||
window: &mut Window,
|
||||
@@ -45,31 +49,34 @@ pub fn init(
|
||||
}
|
||||
}
|
||||
|
||||
struct Message {
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct ChatItem {
|
||||
profile: NostrProfile,
|
||||
content: SharedString,
|
||||
ago: SharedString,
|
||||
}
|
||||
|
||||
impl PartialEq for Message {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let content = self.content == other.content;
|
||||
let member = self.profile == other.profile;
|
||||
let ago = self.ago == other.ago;
|
||||
|
||||
content && member && ago
|
||||
}
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum Message {
|
||||
Item(Box<ChatItem>),
|
||||
System(SharedString),
|
||||
Placeholder,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn new(profile: NostrProfile, content: SharedString, ago: SharedString) -> Self {
|
||||
Self {
|
||||
profile,
|
||||
content,
|
||||
ago,
|
||||
}
|
||||
pub fn new(chat_message: ChatItem) -> Self {
|
||||
Self::Item(Box::new(chat_message))
|
||||
}
|
||||
|
||||
pub fn system(content: SharedString) -> Self {
|
||||
Self::System(content)
|
||||
}
|
||||
|
||||
pub fn placeholder() -> Self {
|
||||
Self::Placeholder
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Chat {
|
||||
// Panel
|
||||
id: SharedString,
|
||||
@@ -95,7 +102,7 @@ impl Chat {
|
||||
let new_messages = model.read(cx).new_messages.downgrade();
|
||||
|
||||
cx.new(|cx| {
|
||||
let messages = cx.new(|_| Vec::new());
|
||||
let messages = cx.new(|_| vec![Message::placeholder()]);
|
||||
let attaches = cx.new(|_| None);
|
||||
|
||||
let input = cx.new(|cx| {
|
||||
@@ -140,8 +147,12 @@ impl Chat {
|
||||
subscriptions,
|
||||
};
|
||||
|
||||
// Verify messaging relays of all members
|
||||
this.verify_messaging_relays(cx);
|
||||
|
||||
// Load all messages from database
|
||||
this.load_messages(cx);
|
||||
|
||||
// Subscribe and load new messages
|
||||
this.load_new_messages(cx);
|
||||
|
||||
@@ -149,6 +160,62 @@ impl Chat {
|
||||
})
|
||||
}
|
||||
|
||||
fn verify_messaging_relays(&self, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let room = model.read(cx);
|
||||
let pubkeys: Vec<PublicKey> = room.members.iter().map(|m| m.public_key()).collect();
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<Vec<(PublicKey, bool)>>();
|
||||
|
||||
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 {
|
||||
if let Ok(result) = rx.await {
|
||||
_ = cx.update(|cx| {
|
||||
_ = this.update(cx, |this, cx| {
|
||||
for item in result.into_iter() {
|
||||
if !item.1 {
|
||||
let name = this
|
||||
.room
|
||||
.read_with(cx, |this, _| this.name())
|
||||
.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn load_messages(&self, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.room.upgrade() else {
|
||||
return;
|
||||
@@ -199,11 +266,55 @@ impl Chat {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn push_system_message(&self, content: String, cx: &mut Context<Self>) {
|
||||
let old_len = self.messages.read(cx).len();
|
||||
let message = Message::system(content.into());
|
||||
|
||||
cx.update_entity(&self.messages, |this, cx| {
|
||||
this.extend(vec![message]);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
}
|
||||
|
||||
fn push_message(&self, content: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let old_len = self.messages.read(cx).len();
|
||||
let room = model.read(cx);
|
||||
let ago = LastSeen(Timestamp::now()).human_readable();
|
||||
let message = Message::new(ChatItem {
|
||||
profile: room.owner.clone(),
|
||||
content: content.into(),
|
||||
ago,
|
||||
});
|
||||
|
||||
// Update message list
|
||||
cx.update_entity(&self.messages, |this, cx| {
|
||||
this.extend(vec![message]);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
// Reset message input
|
||||
cx.update_entity(&self.input, |this, cx| {
|
||||
this.set_loading(false, window, cx);
|
||||
this.set_disabled(false, window, cx);
|
||||
this.set_text("", window, cx);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
}
|
||||
|
||||
fn push_messages(&self, events: Events, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let old_len = self.messages.read(cx).len();
|
||||
let room = model.read(cx);
|
||||
let pubkeys = room.pubkeys();
|
||||
|
||||
@@ -224,11 +335,11 @@ impl Chat {
|
||||
room.owner.to_owned()
|
||||
};
|
||||
|
||||
Some(Message::new(
|
||||
member,
|
||||
ev.content.into(),
|
||||
LastSeen(ev.created_at).human_readable(),
|
||||
))
|
||||
Some(Message::new(ChatItem {
|
||||
profile: member,
|
||||
content: ev.content.into(),
|
||||
ago: LastSeen(ev.created_at).human_readable(),
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -244,7 +355,7 @@ impl Chat {
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self.list_state.reset(total);
|
||||
self.list_state.splice(old_len..old_len, total);
|
||||
}
|
||||
|
||||
fn load_new_messages(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -266,11 +377,11 @@ impl Chat {
|
||||
.iter()
|
||||
.filter_map(|event| {
|
||||
if let Some(profile) = room.member(&event.pubkey) {
|
||||
let message = Message::new(
|
||||
let message = Message::new(ChatItem {
|
||||
profile,
|
||||
event.content.clone().into(),
|
||||
LastSeen(event.created_at).human_readable(),
|
||||
);
|
||||
content: event.content.clone().into(),
|
||||
ago: LastSeen(event.created_at).human_readable(),
|
||||
});
|
||||
|
||||
if !old_messages.iter().any(|old| old == &message) {
|
||||
Some(message)
|
||||
@@ -339,6 +450,7 @@ impl Chat {
|
||||
|
||||
let client = get_client();
|
||||
let window_handle = window.window_handle();
|
||||
let (tx, rx) = oneshot::channel::<Vec<Error>>();
|
||||
|
||||
let room = model.read(cx);
|
||||
let pubkeys = room.pubkeys();
|
||||
@@ -357,55 +469,43 @@ impl Chat {
|
||||
|
||||
// Send message to all pubkeys
|
||||
cx.background_spawn(async move {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for pubkey in pubkeys.iter() {
|
||||
if let Err(_e) = client
|
||||
if let Err(e) = client
|
||||
.send_private_msg(*pubkey, &async_content, tags.clone())
|
||||
.await
|
||||
{
|
||||
// TODO: handle error
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
_ = tx.send(errors);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||
_ = this.update(cx, |this, cx| {
|
||||
this.force_push_message(content.clone(), window, cx);
|
||||
this.push_message(content.clone(), window, cx);
|
||||
});
|
||||
});
|
||||
|
||||
if let Ok(errors) = rx.await {
|
||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||
for error in errors.into_iter() {
|
||||
window.push_notification(
|
||||
Notification::error(error.to_string()).title("Message Failed to Send"),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn force_push_message(&self, content: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let room = model.read(cx);
|
||||
let ago = LastSeen(Timestamp::now()).human_readable();
|
||||
let message = Message::new(room.owner.clone(), content.into(), ago);
|
||||
let old_len = self.messages.read(cx).len();
|
||||
|
||||
// Update message list
|
||||
cx.update_entity(&self.messages, |this, cx| {
|
||||
this.extend(vec![message]);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
// Reset message input
|
||||
cx.update_entity(&self.input, |this, cx| {
|
||||
this.set_loading(false, window, cx);
|
||||
this.set_disabled(false, window, cx);
|
||||
this.set_text("", window, cx);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
}
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn upload_media(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
@@ -467,7 +567,7 @@ impl Chat {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn remove(&mut self, url: &Url, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn remove_media(&mut self, url: &Url, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.attaches.update(cx, |model, cx| {
|
||||
if let Some(urls) = model.as_mut() {
|
||||
let ix = urls.iter().position(|x| x == url).unwrap();
|
||||
@@ -496,46 +596,86 @@ impl Chat {
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().transparent)
|
||||
.group_hover("", |this| {
|
||||
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
img(message.profile.avatar())
|
||||
.size_8()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_initial()
|
||||
.overflow_hidden()
|
||||
.map(|this| match message {
|
||||
Message::Item(item) => this
|
||||
.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().transparent)
|
||||
.group_hover("", |this| {
|
||||
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
img(item.profile.avatar())
|
||||
.size_8()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_baseline()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.child(div().font_semibold().child(message.profile.name()))
|
||||
.flex_col()
|
||||
.flex_initial()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div().child(message.ago.clone()).text_color(
|
||||
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
||||
),
|
||||
),
|
||||
div()
|
||||
.flex()
|
||||
.items_baseline()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.child(div().font_semibold().child(item.profile.name()))
|
||||
.child(div().child(item.ago.clone()).text_color(
|
||||
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
||||
)),
|
||||
)
|
||||
.child(div().text_sm().child(item.content.clone())),
|
||||
),
|
||||
Message::System(content) => this
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().transparent)
|
||||
.group_hover("", |this| this.bg(cx.theme().danger)),
|
||||
)
|
||||
.child(div().text_sm().child(message.content.clone())),
|
||||
)
|
||||
.child(
|
||||
img("brand/avatar.png")
|
||||
.size_8()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger)
|
||||
.child(content.clone()),
|
||||
Message::Placeholder => this
|
||||
.w_full()
|
||||
.h_32()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.line_height(relative(1.))
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_8()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
||||
)
|
||||
.child(ALERT),
|
||||
})
|
||||
} else {
|
||||
div()
|
||||
}
|
||||
@@ -651,7 +791,7 @@ impl Render for Chat {
|
||||
),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.remove(&url, window, cx);
|
||||
this.remove_media(&url, window, cx);
|
||||
}))
|
||||
}))
|
||||
})
|
||||
@@ -667,7 +807,7 @@ impl Render for Chat {
|
||||
.icon(Icon::new(IconName::Upload))
|
||||
.ghost()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.upload(window, cx);
|
||||
this.upload_media(window, cx);
|
||||
}))
|
||||
.loading(self.is_uploading),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use common::{profile::NostrProfile, qr::create_qr, utils::preload};
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, relative, svg, App, AppContext, ClipboardItem, Context, Div,
|
||||
Entity, IntoElement, ParentElement, Render, Styled, Window,
|
||||
Entity, IntoElement, ParentElement, Render, Styled, Subscription, Window,
|
||||
};
|
||||
use nostr_connect::prelude::*;
|
||||
use state::get_client;
|
||||
@@ -17,7 +17,8 @@ use ui::{
|
||||
|
||||
use super::app;
|
||||
|
||||
const ALPHA_MESSAGE: &str = "Coop is in the alpha stage; it doesn't store any credentials. You will need to log in again when you relaunch.";
|
||||
const ALPHA_MESSAGE: &str =
|
||||
"Coop is in the alpha stage of development; It may contain bugs, unfinished features, or unexpected behavior.";
|
||||
const JOIN_URL: &str = "https://start.njump.me/";
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
@@ -32,6 +33,8 @@ pub struct Onboarding {
|
||||
use_connect: bool,
|
||||
use_privkey: bool,
|
||||
is_loading: bool,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl Onboarding {
|
||||
@@ -55,7 +58,7 @@ impl Onboarding {
|
||||
|
||||
cx.new(|cx| {
|
||||
// Handle Enter event for nsec input
|
||||
cx.subscribe_in(
|
||||
let subscriptions = vec![cx.subscribe_in(
|
||||
&nsec_input,
|
||||
window,
|
||||
move |this: &mut Self, _, input_event, window, cx| {
|
||||
@@ -63,8 +66,7 @@ impl Onboarding {
|
||||
this.privkey_login(window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
)];
|
||||
|
||||
Self {
|
||||
app_keys,
|
||||
@@ -74,6 +76,7 @@ impl Onboarding {
|
||||
use_connect: false,
|
||||
use_privkey: false,
|
||||
is_loading: false,
|
||||
subscriptions,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use chats::registry::ChatRegistry;
|
||||
use chats::{registry::ChatRegistry, room::Room};
|
||||
use common::{
|
||||
constants::FAKE_SIG,
|
||||
profile::NostrProfile,
|
||||
utils::{random_name, signer_public_key},
|
||||
};
|
||||
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, TextAlign, Window,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, TextAlign, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use smol::Timer;
|
||||
use state::get_client;
|
||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
use tokio::sync::oneshot;
|
||||
use ui::{
|
||||
button::{Button, ButtonRounded},
|
||||
@@ -21,6 +21,9 @@ use ui::{
|
||||
ContextModal, Icon, IconName, Sizable, Size, StyledExt,
|
||||
};
|
||||
|
||||
const ALERT: &str =
|
||||
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
struct SelectContact(PublicKey);
|
||||
|
||||
@@ -28,26 +31,23 @@ impl_internal_actions!(contacts, [SelectContact]);
|
||||
|
||||
pub struct Compose {
|
||||
title_input: Entity<TextInput>,
|
||||
message_input: Entity<TextInput>,
|
||||
user_input: Entity<TextInput>,
|
||||
contacts: Entity<Vec<NostrProfile>>,
|
||||
selected: Entity<HashSet<PublicKey>>,
|
||||
focus_handle: FocusHandle,
|
||||
is_loading: bool,
|
||||
is_submitting: bool,
|
||||
error_message: Entity<Option<SharedString>>,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl Compose {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
|
||||
let contacts = cx.new(|_| Vec::new());
|
||||
let selected = cx.new(|_| HashSet::new());
|
||||
|
||||
let user_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(ui::Size::Small)
|
||||
.small()
|
||||
.placeholder("npub1...")
|
||||
});
|
||||
let error_message = cx.new(|_| None);
|
||||
let mut subscriptions = Vec::new();
|
||||
|
||||
let title_input = cx.new(|cx| {
|
||||
let name = random_name(2);
|
||||
@@ -60,15 +60,15 @@ impl Compose {
|
||||
input
|
||||
});
|
||||
|
||||
let message_input = cx.new(|cx| {
|
||||
let user_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.appearance(false)
|
||||
.text_size(Size::XSmall)
|
||||
.placeholder("Hello...")
|
||||
.text_size(ui::Size::Small)
|
||||
.small()
|
||||
.placeholder("npub1...")
|
||||
});
|
||||
|
||||
// Handle Enter event for message input
|
||||
cx.subscribe_in(
|
||||
// Handle Enter event for user input
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&user_input,
|
||||
window,
|
||||
move |this, _, input_event, window, cx| {
|
||||
@@ -76,155 +76,118 @@ impl Compose {
|
||||
this.add(window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
));
|
||||
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(public_key) = signer_public_key(client).await {
|
||||
if let Ok(profiles) = client.database().contacts(public_key).await {
|
||||
let members: Vec<NostrProfile> = profiles
|
||||
.into_iter()
|
||||
.map(|profile| NostrProfile::new(profile.public_key(), profile.metadata()))
|
||||
.collect();
|
||||
|
||||
_ = tx.send(members);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let client = get_client();
|
||||
if let Ok(public_key) = signer_public_key(client).await {
|
||||
if let Ok(profiles) = client.database().contacts(public_key).await {
|
||||
let members: Vec<NostrProfile> = profiles
|
||||
.into_iter()
|
||||
.map(|profile| {
|
||||
NostrProfile::new(profile.public_key(), profile.metadata())
|
||||
})
|
||||
.collect();
|
||||
|
||||
_ = tx.send(members);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
if let Ok(contacts) = rx.await {
|
||||
if let Some(view) = this.upgrade() {
|
||||
_ = cx.update_entity(&view, |this, cx| {
|
||||
_ = cx.update(|cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.contacts.update(cx, |this, cx| {
|
||||
this.extend(contacts);
|
||||
cx.notify();
|
||||
});
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
title_input,
|
||||
message_input,
|
||||
user_input,
|
||||
contacts,
|
||||
selected,
|
||||
error_message,
|
||||
is_loading: false,
|
||||
is_submitting: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let selected = self.selected.read(cx).to_owned();
|
||||
let message = self.message_input.read(cx).text();
|
||||
|
||||
if selected.is_empty() {
|
||||
window.push_notification("You need to add at least 1 receiver", cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if message.is_empty() {
|
||||
window.push_notification("Message is required", cx);
|
||||
if self.selected.read(cx).is_empty() {
|
||||
self.set_error(Some("You need to add at least 1 receiver".into()), cx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner
|
||||
self.set_submitting(true, cx);
|
||||
|
||||
// Get message from user's input
|
||||
let content = message.to_string();
|
||||
|
||||
// Get room title from user's input
|
||||
let title = Tag::custom(
|
||||
TagKind::Subject,
|
||||
vec![self.title_input.read(cx).text().to_string()],
|
||||
);
|
||||
|
||||
// Get all pubkeys
|
||||
let mut pubkeys: Vec<PublicKey> = selected.iter().copied().collect();
|
||||
let pubkeys: Vec<PublicKey> = self.selected.read(cx).iter().copied().collect();
|
||||
|
||||
// Convert selected pubkeys into Nostr tags
|
||||
let mut tag_list: Vec<Tag> = selected.iter().map(|pk| Tag::public_key(*pk)).collect();
|
||||
tag_list.push(title);
|
||||
let mut tag_list: Vec<Tag> = pubkeys.iter().map(|pk| Tag::public_key(*pk)).collect();
|
||||
|
||||
// Add subject if it is present
|
||||
if !self.title_input.read(cx).text().is_empty() {
|
||||
tag_list.push(Tag::custom(
|
||||
TagKind::Subject,
|
||||
vec![self.title_input.read(cx).text().to_string()],
|
||||
));
|
||||
}
|
||||
|
||||
let tags = Tags::new(tag_list);
|
||||
let client = get_client();
|
||||
let window_handle = window.window_handle();
|
||||
let (tx, rx) = oneshot::channel::<Event>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let signer = client.signer().await.expect("Signer is required");
|
||||
// [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(), "")
|
||||
.tags(tags)
|
||||
.sign(&signer)
|
||||
.await
|
||||
{
|
||||
_ = tx.send(event)
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (tx, rx) = oneshot::channel::<Event>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let public_key = signer_public_key(client).await.unwrap();
|
||||
let mut event: Option<Event> = None;
|
||||
|
||||
pubkeys.push(public_key);
|
||||
|
||||
for pubkey in pubkeys.iter() {
|
||||
if let Ok(output) = client
|
||||
.send_private_msg(*pubkey, &content, tags.clone())
|
||||
.await
|
||||
{
|
||||
if pubkey == &public_key && event.is_none() {
|
||||
if let Ok(Some(ev)) = client.database().event_by_id(&output.val).await {
|
||||
if let Ok(UnwrappedGift { mut rumor, .. }) =
|
||||
client.unwrap_gift_wrap(&ev).await
|
||||
{
|
||||
// Compute event id if not exist
|
||||
rumor.ensure_id();
|
||||
|
||||
if let Some(id) = rumor.id {
|
||||
let ev = Event::new(
|
||||
id,
|
||||
rumor.pubkey,
|
||||
rumor.created_at,
|
||||
rumor.kind,
|
||||
rumor.tags,
|
||||
rumor.content,
|
||||
Signature::from_str(FAKE_SIG).unwrap(),
|
||||
);
|
||||
|
||||
event = Some(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(event) = event {
|
||||
_ = tx.send(event);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Ok(event) = rx.await {
|
||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||
if let Some(chats) = ChatRegistry::global(cx) {
|
||||
chats.update(cx, |this, cx| {
|
||||
this.push_message(event, cx);
|
||||
});
|
||||
}
|
||||
|
||||
// Stop loading spinner
|
||||
_ = this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
});
|
||||
|
||||
// Close modal
|
||||
window.close_modal(cx);
|
||||
if let Some(chats) = ChatRegistry::global(cx) {
|
||||
let room = Room::parse(&event, cx);
|
||||
|
||||
chats.update(cx, |state, cx| match state.new_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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -303,10 +266,29 @@ impl Compose {
|
||||
.detach();
|
||||
} else {
|
||||
self.set_loading(false, cx);
|
||||
window.push_notification("Public Key is not valid", cx);
|
||||
self.set_error(Some("Public Key is not valid".into()), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_error(&mut self, error: Option<SharedString>, cx: &mut Context<Self>) {
|
||||
self.error_message.update(cx, |this, cx| {
|
||||
*this = error;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
// Dismiss error after 2 seconds
|
||||
cx.spawn(|this, cx| async move {
|
||||
Timer::after(Duration::from_secs(2)).await;
|
||||
|
||||
_ = cx.update(|cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(None, cx);
|
||||
})
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_loading = status;
|
||||
cx.notify();
|
||||
@@ -336,9 +318,6 @@ impl Compose {
|
||||
|
||||
impl Render for Compose {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let msg =
|
||||
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_action_select))
|
||||
@@ -350,36 +329,30 @@ impl Render for Compose {
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.child(msg),
|
||||
.child(ALERT),
|
||||
)
|
||||
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
||||
this.child(
|
||||
div()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger)
|
||||
.child(msg.clone()),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
div()
|
||||
.h_10()
|
||||
.px_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().text_xs().font_semibold().child("Title:"))
|
||||
.child(self.title_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h_10()
|
||||
.px_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().text_xs().font_semibold().child("Message:"))
|
||||
.child(self.message_input.clone()),
|
||||
),
|
||||
div().flex().flex_col().child(
|
||||
div()
|
||||
.h_10()
|
||||
.px_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().text_xs().font_semibold().child("Title:"))
|
||||
.child(self.title_input.clone()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::anyhow;
|
||||
use async_utility::tokio::sync::oneshot;
|
||||
use common::utils::{compare, room_hash, signer_public_key};
|
||||
use gpui::{App, AppContext, Context, Entity, Global};
|
||||
@@ -151,6 +152,21 @@ impl ChatRegistry {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn new_room(&mut self, room: Room, cx: &mut Context<Self>) -> Result<(), anyhow::Error> {
|
||||
if !self
|
||||
.rooms
|
||||
.iter()
|
||||
.any(|current| compare(¤t.read(cx).pubkeys(), &room.pubkeys()))
|
||||
{
|
||||
self.rooms.insert(0, cx.new(|_| room));
|
||||
cx.notify();
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Room is existed"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_message(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||
// Get all pubkeys from event's tags for comparision
|
||||
let mut pubkeys: Vec<_> = event.tags.public_keys().copied().collect();
|
||||
|
||||
@@ -19,6 +19,8 @@ impl AsRef<Metadata> for NostrProfile {
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for NostrProfile {}
|
||||
|
||||
impl PartialEq for NostrProfile {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.public_key() == other.public_key()
|
||||
|
||||
@@ -97,6 +97,7 @@ impl From<(NotificationType, SharedString)> for Notification {
|
||||
}
|
||||
|
||||
struct DefaultIdType;
|
||||
|
||||
impl Notification {
|
||||
/// Create a new notification with the given content.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user