feat: improve compose modal
This commit is contained in:
@@ -326,20 +326,26 @@ fn main() {
|
|||||||
while let Some(signal) = signal_rx.recv().await {
|
while let Some(signal) = signal_rx.recv().await {
|
||||||
match signal {
|
match signal {
|
||||||
Signal::Eose => {
|
Signal::Eose => {
|
||||||
cx.update_window(*window.deref(), |_this, _window, cx| {
|
if let Err(e) =
|
||||||
cx.update_global::<ChatRegistry, _>(|this, cx| {
|
cx.update_window(*window.deref(), |_this, window, cx| {
|
||||||
this.load(cx);
|
cx.update_global::<ChatRegistry, _>(|this, cx| {
|
||||||
});
|
this.load(window, cx);
|
||||||
})
|
});
|
||||||
.unwrap();
|
})
|
||||||
|
{
|
||||||
|
error!("Error: {}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Signal::Event(event) => {
|
Signal::Event(event) => {
|
||||||
cx.update_window(*window.deref(), |_this, _window, cx| {
|
if let Err(e) =
|
||||||
cx.update_global::<ChatRegistry, _>(|this, cx| {
|
cx.update_window(*window.deref(), |_this, window, cx| {
|
||||||
this.new_room_message(event, cx);
|
cx.update_global::<ChatRegistry, _>(|this, cx| {
|
||||||
});
|
this.new_room_message(event, window, cx);
|
||||||
})
|
});
|
||||||
.unwrap();
|
})
|
||||||
|
{
|
||||||
|
error!("Error: {}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ impl AppView {
|
|||||||
PanelKind::Room(id) => {
|
PanelKind::Room(id) => {
|
||||||
if let Some(weak_room) = cx.global::<ChatRegistry>().get_room(id, cx) {
|
if let Some(weak_room) = cx.global::<ChatRegistry>().get_room(id, cx) {
|
||||||
if let Some(room) = weak_room.upgrade() {
|
if let Some(room) = weak_room.upgrade() {
|
||||||
let panel = Arc::new(chat::init(room, window, cx));
|
let panel = Arc::new(chat::init(&room, window, cx));
|
||||||
|
|
||||||
self.dock.update(cx, |dock_area, cx| {
|
self.dock.update(cx, |dock_area, cx| {
|
||||||
dock_area.add_panel(panel, action.position, window, cx);
|
dock_area.add_panel(panel, action.position, window, cx);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use ui::{
|
|||||||
|
|
||||||
#[derive(Clone, Debug, IntoElement)]
|
#[derive(Clone, Debug, IntoElement)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
member: NostrProfile,
|
profile: NostrProfile,
|
||||||
content: SharedString,
|
content: SharedString,
|
||||||
ago: SharedString,
|
ago: SharedString,
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ pub struct Message {
|
|||||||
impl PartialEq for Message {
|
impl PartialEq for Message {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
let content = self.content == other.content;
|
let content = self.content == other.content;
|
||||||
let member = self.member == other.member;
|
let member = self.profile == other.profile;
|
||||||
let ago = self.ago == other.ago;
|
let ago = self.ago == other.ago;
|
||||||
|
|
||||||
content && member && ago
|
content && member && ago
|
||||||
@@ -26,9 +26,9 @@ impl PartialEq for Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
pub fn new(member: NostrProfile, content: SharedString, ago: SharedString) -> Self {
|
pub fn new(profile: NostrProfile, content: SharedString, ago: SharedString) -> Self {
|
||||||
Self {
|
Self {
|
||||||
member,
|
profile,
|
||||||
content,
|
content,
|
||||||
ago,
|
ago,
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ impl RenderOnce for Message {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
img(self.member.avatar())
|
img(self.profile.avatar())
|
||||||
.size_8()
|
.size_8()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.flex_shrink_0(),
|
.flex_shrink_0(),
|
||||||
@@ -75,7 +75,7 @@ impl RenderOnce for Message {
|
|||||||
.items_baseline()
|
.items_baseline()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.child(div().font_semibold().child(self.member.name()))
|
.child(div().font_semibold().child(self.profile.name()))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.child(self.ago)
|
.child(self.ago)
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ use async_utility::task::spawn;
|
|||||||
use chat_state::room::Room;
|
use chat_state::room::Room;
|
||||||
use common::{
|
use common::{
|
||||||
constants::IMAGE_SERVICE,
|
constants::IMAGE_SERVICE,
|
||||||
|
profile::NostrProfile,
|
||||||
utils::{compare, message_time, nip96_upload},
|
utils::{compare, message_time, nip96_upload},
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, list, prelude::FluentBuilder, px, white, AnyElement, App, AppContext, Context,
|
div, img, list, prelude::FluentBuilder, px, white, AnyElement, App, AppContext, Context,
|
||||||
Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement,
|
Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement,
|
||||||
ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Pixels, Render,
|
ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Pixels, Render,
|
||||||
SharedString, StatefulInteractiveElement, Styled, StyledImage, WeakEntity, Window,
|
SharedString, StatefulInteractiveElement, Styled, StyledImage, Window,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use message::Message;
|
use message::Message;
|
||||||
@@ -27,7 +28,7 @@ use ui::{
|
|||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
|
|
||||||
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> {
|
pub fn init(room: &Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> {
|
||||||
Chat::new(room, window, cx)
|
Chat::new(room, window, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +46,8 @@ pub struct Chat {
|
|||||||
// Chat Room
|
// Chat Room
|
||||||
id: SharedString,
|
id: SharedString,
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
room: Entity<Room>,
|
owner: NostrProfile,
|
||||||
|
members: Vec<NostrProfile>,
|
||||||
state: Entity<State>,
|
state: Entity<State>,
|
||||||
list: ListState,
|
list: ListState,
|
||||||
// New Message
|
// New Message
|
||||||
@@ -56,12 +58,15 @@ pub struct Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Chat {
|
impl Chat {
|
||||||
pub fn new(model: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub fn new(model: &Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
let room = model.read(cx);
|
let room = model.read(cx);
|
||||||
let id = room.id.to_string().into();
|
let id = room.id.to_string().into();
|
||||||
let name = room.title.clone().unwrap_or("Untitled".into());
|
let name = room.title.clone().unwrap_or("Untitled".into());
|
||||||
|
let owner = room.owner.clone();
|
||||||
|
let members = room.members.clone();
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
|
// Load all messages
|
||||||
cx.observe_new::<Self>(|this, window, cx| {
|
cx.observe_new::<Self>(|this, window, cx| {
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
this.load_messages(window, cx);
|
this.load_messages(window, cx);
|
||||||
@@ -69,7 +74,13 @@ impl Chat {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
// Form
|
// Observe and load new messages
|
||||||
|
cx.observe_in(model, window, |this: &mut Chat, model, _, cx| {
|
||||||
|
this.load_new_messages(&model, cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
// New message form
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
TextInput::new(window, cx)
|
TextInput::new(window, cx)
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
@@ -77,60 +88,56 @@ impl Chat {
|
|||||||
.placeholder("Message...")
|
.placeholder("Message...")
|
||||||
});
|
});
|
||||||
|
|
||||||
// List
|
|
||||||
let state = cx.new(|_| State {
|
|
||||||
count: 0,
|
|
||||||
items: vec![],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send message when user presses enter
|
// Send message when user presses enter
|
||||||
cx.subscribe_in(
|
cx.subscribe_in(
|
||||||
&input,
|
&input,
|
||||||
window,
|
window,
|
||||||
move |this: &mut Chat, view, input_event, window, cx| {
|
move |this: &mut Chat, _, input_event, window, cx| {
|
||||||
if let InputEvent::PressEnter = input_event {
|
if let InputEvent::PressEnter = input_event {
|
||||||
this.send_message(view.downgrade(), window, cx);
|
this.send_message(window, cx);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
// List state model
|
||||||
|
let state = cx.new(|_| State {
|
||||||
|
count: 0,
|
||||||
|
items: vec![],
|
||||||
|
});
|
||||||
|
|
||||||
// Update list on every state changes
|
// Update list on every state changes
|
||||||
cx.observe(&state, |this, model, cx| {
|
cx.observe(&state, |this, model, cx| {
|
||||||
let items = model.read(cx).items.clone();
|
|
||||||
|
|
||||||
this.list = ListState::new(
|
this.list = ListState::new(
|
||||||
items.len(),
|
model.read(cx).items.len(),
|
||||||
ListAlignment::Bottom,
|
ListAlignment::Bottom,
|
||||||
Pixels(256.),
|
Pixels(1024.),
|
||||||
move |idx, _window, _cx| {
|
move |idx, _window, cx| {
|
||||||
let item = items.get(idx).unwrap().clone();
|
if let Some(message) = model.read(cx).items.get(idx) {
|
||||||
div().child(item).into_any_element()
|
div().child(message.clone()).into_any_element()
|
||||||
|
} else {
|
||||||
|
div().into_any_element()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
cx.observe_in(&model, window, |this, model, window, cx| {
|
|
||||||
this.load_new_messages(model.downgrade(), window, cx);
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let attaches = cx.new(|_| None);
|
let attaches = cx.new(|_| None);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
closable: true,
|
closable: true,
|
||||||
zoomable: true,
|
zoomable: true,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
room: model,
|
list: ListState::new(0, ListAlignment::Bottom, Pixels(1024.), move |_, _, _| {
|
||||||
list: ListState::new(0, ListAlignment::Bottom, Pixels(256.), move |_, _, _| {
|
|
||||||
div().into_any_element()
|
div().into_any_element()
|
||||||
}),
|
}),
|
||||||
is_uploading: false,
|
is_uploading: false,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
owner,
|
||||||
|
members,
|
||||||
input,
|
input,
|
||||||
state,
|
state,
|
||||||
attaches,
|
attaches,
|
||||||
@@ -138,150 +145,139 @@ impl Chat {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_messages(&self, _window: &mut Window, cx: &mut Context<Self>) {
|
fn load_messages(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let room = self.room.read(cx);
|
let window_handle = window.window_handle();
|
||||||
let members = room.members.clone();
|
// Get current user
|
||||||
let owner = room.owner.clone();
|
let author = self.owner.public_key();
|
||||||
// Get all public keys
|
// Get other users in room
|
||||||
let all_keys = room.get_pubkeys();
|
let pubkeys = self
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.public_key())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
// Get all public keys for comparisation
|
||||||
|
let mut all_keys = pubkeys.clone();
|
||||||
|
all_keys.push(author);
|
||||||
|
|
||||||
// Async
|
cx.spawn(|this, mut cx| async move {
|
||||||
let async_state = self.state.clone();
|
let (tx, rx) = oneshot::channel::<Events>();
|
||||||
let mut async_cx = cx.to_async();
|
|
||||||
|
|
||||||
cx.foreground_executor()
|
cx.background_spawn({
|
||||||
.spawn(async move {
|
let client = get_client();
|
||||||
let events: anyhow::Result<Events, anyhow::Error> = async_cx
|
|
||||||
.background_executor()
|
|
||||||
.spawn({
|
|
||||||
let client = get_client();
|
|
||||||
let pubkeys = members.iter().map(|m| m.public_key()).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
async move {
|
let recv = Filter::new()
|
||||||
let signer = client.signer().await?;
|
.kind(Kind::PrivateDirectMessage)
|
||||||
let author = signer.get_public_key().await?;
|
.author(author)
|
||||||
|
.pubkeys(pubkeys.iter().copied());
|
||||||
|
|
||||||
let recv = Filter::new()
|
let send = Filter::new()
|
||||||
.kind(Kind::PrivateDirectMessage)
|
.kind(Kind::PrivateDirectMessage)
|
||||||
.author(author)
|
.authors(pubkeys)
|
||||||
.pubkeys(pubkeys.clone());
|
.pubkey(author);
|
||||||
|
|
||||||
let send = Filter::new()
|
// Get all DM events in database
|
||||||
.kind(Kind::PrivateDirectMessage)
|
async move {
|
||||||
.authors(pubkeys)
|
let recv_events = client.database().query(recv).await.unwrap();
|
||||||
.pubkey(author);
|
let send_events = client.database().query(send).await.unwrap();
|
||||||
|
let events = recv_events.merge(send_events);
|
||||||
// Get all DM events in database
|
_ = tx.send(events);
|
||||||
let recv_events = client.database().query(recv).await?;
|
|
||||||
let send_events = client.database().query(send).await?;
|
|
||||||
let events = recv_events.merge(send_events);
|
|
||||||
|
|
||||||
Ok(events)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(events) = events {
|
|
||||||
let items: Vec<Message> = events
|
|
||||||
.into_iter()
|
|
||||||
.sorted_by_key(|ev| ev.created_at)
|
|
||||||
.filter_map(|ev| {
|
|
||||||
let mut pubkeys: Vec<_> = ev.tags.public_keys().copied().collect();
|
|
||||||
pubkeys.push(ev.pubkey);
|
|
||||||
|
|
||||||
if compare(&pubkeys, &all_keys) {
|
|
||||||
let member = if let Some(member) =
|
|
||||||
members.iter().find(|&m| m.public_key() == ev.pubkey)
|
|
||||||
{
|
|
||||||
member.to_owned()
|
|
||||||
} else {
|
|
||||||
owner.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Message::new(
|
|
||||||
member,
|
|
||||||
ev.content.into(),
|
|
||||||
message_time(ev.created_at).into(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let total = items.len();
|
|
||||||
|
|
||||||
_ = async_cx.update_entity(&async_state, |a, b| {
|
|
||||||
a.items = items;
|
|
||||||
a.count = total;
|
|
||||||
b.notify();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
if let Ok(events) = rx.await {
|
||||||
|
_ = cx.update_window(window_handle, |_, _, cx| {
|
||||||
|
_ = this.update(cx, |this, cx| {
|
||||||
|
let items: Vec<Message> = events
|
||||||
|
.into_iter()
|
||||||
|
.sorted_by_key(|ev| ev.created_at)
|
||||||
|
.filter_map(|ev| {
|
||||||
|
let mut pubkeys: Vec<_> = ev.tags.public_keys().copied().collect();
|
||||||
|
pubkeys.push(ev.pubkey);
|
||||||
|
|
||||||
|
if compare(&pubkeys, &all_keys) {
|
||||||
|
let member = if let Some(member) =
|
||||||
|
this.members.iter().find(|&m| m.public_key() == ev.pubkey)
|
||||||
|
{
|
||||||
|
member.to_owned()
|
||||||
|
} else {
|
||||||
|
this.owner.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Message::new(
|
||||||
|
member,
|
||||||
|
ev.content.into(),
|
||||||
|
message_time(ev.created_at).into(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
cx.update_entity(&this.state, |this, cx| {
|
||||||
|
this.count = items.len();
|
||||||
|
this.items = items;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_new_messages(
|
fn load_new_messages(&self, model: &Entity<Room>, cx: &mut Context<Self>) {
|
||||||
&self,
|
let room = model.read(cx);
|
||||||
model: WeakEntity<Room>,
|
let items: Vec<Message> = room
|
||||||
_window: &mut Window,
|
.new_messages
|
||||||
cx: &mut Context<Self>,
|
.iter()
|
||||||
) {
|
.filter_map(|event| {
|
||||||
if let Some(model) = model.upgrade() {
|
room.member(&event.pubkey).map(|member| {
|
||||||
let room = model.read(cx);
|
Message::new(
|
||||||
let items: Vec<Message> = room
|
member,
|
||||||
.new_messages
|
event.content.clone().into(),
|
||||||
.iter()
|
message_time(event.created_at).into(),
|
||||||
.filter_map(|event| {
|
)
|
||||||
room.member(&event.pubkey).map(|member| {
|
})
|
||||||
Message::new(
|
})
|
||||||
member,
|
.collect();
|
||||||
event.content.clone().into(),
|
|
||||||
message_time(event.created_at).into(),
|
cx.update_entity(&self.state, |this, cx| {
|
||||||
)
|
let messages: Vec<Message> = items
|
||||||
})
|
.into_iter()
|
||||||
|
.filter_map(|new| {
|
||||||
|
if !this.items.iter().any(|old| old == &new) {
|
||||||
|
Some(new)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
cx.update_entity(&self.state, |model, cx| {
|
this.items.extend(messages);
|
||||||
let messages: Vec<Message> = items
|
this.count = this.items.len();
|
||||||
.into_iter()
|
cx.notify();
|
||||||
.filter_map(|new| {
|
});
|
||||||
if !model.items.iter().any(|old| old == &new) {
|
|
||||||
Some(new)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
model.items.extend(messages);
|
|
||||||
model.count = model.items.len();
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_message(
|
fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
&mut self,
|
|
||||||
view: WeakEntity<TextInput>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let window_handle = window.window_handle();
|
let window_handle = window.window_handle();
|
||||||
let room = self.room.read(cx);
|
|
||||||
let owner = room.owner.clone();
|
// Get current user
|
||||||
let mut members = room.members.to_vec();
|
let author = self.owner.public_key();
|
||||||
members.push(owner.clone());
|
|
||||||
|
// Get other users in room
|
||||||
|
let mut pubkeys = self
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.public_key())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
pubkeys.push(author);
|
||||||
|
|
||||||
// Get message
|
// Get message
|
||||||
let mut content = self.input.read(cx).text().to_string();
|
let mut content = self.input.read(cx).text().to_string();
|
||||||
|
|
||||||
if content.is_empty() {
|
|
||||||
window.push_notification("Message cannot be empty", cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all attaches and merge with message
|
// Get all attaches and merge with message
|
||||||
if let Some(attaches) = self.attaches.read(cx).as_ref() {
|
if let Some(attaches) = self.attaches.read(cx).as_ref() {
|
||||||
let merged = attaches
|
let merged = attaches
|
||||||
@@ -293,75 +289,77 @@ impl Chat {
|
|||||||
content = format!("{}\n{}", content, merged)
|
content = format!("{}\n{}", content, merged)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update input state
|
if content.is_empty() {
|
||||||
if let Some(input) = view.upgrade() {
|
window.push_notification("Cannot send an empty message", cx);
|
||||||
cx.update_entity(&input, |input, cx| {
|
return;
|
||||||
input.set_loading(true, window, cx);
|
|
||||||
input.set_disabled(true, window, cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
// Disable input when sending message
|
||||||
// Send message to all members
|
self.input.update(cx, |this, cx| {
|
||||||
cx.background_executor()
|
this.set_loading(true, window, cx);
|
||||||
.spawn({
|
this.set_disabled(true, window, cx);
|
||||||
let client = get_client();
|
});
|
||||||
let content = content.clone().to_string();
|
|
||||||
let tags: Vec<Tag> = members
|
|
||||||
.iter()
|
|
||||||
.filter_map(|m| {
|
|
||||||
if m.public_key() != owner.public_key() {
|
|
||||||
Some(Tag::public_key(m.public_key()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
// Send message to all members
|
cx.background_spawn({
|
||||||
for member in members.iter() {
|
let client = get_client();
|
||||||
_ = client
|
let content = content.clone();
|
||||||
.send_private_msg(member.public_key(), &content, tags.clone())
|
let tags: Vec<Tag> = pubkeys
|
||||||
.await
|
.iter()
|
||||||
|
.filter_map(|pubkey| {
|
||||||
|
if pubkey != &author {
|
||||||
|
Some(Tag::public_key(*pubkey))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
// Send message to all members
|
||||||
|
for pubkey in pubkeys.iter() {
|
||||||
|
if let Err(_e) = client
|
||||||
|
.send_private_msg(*pubkey, &content, tags.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
// TODO: handle error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.detach();
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
if let Some(view) = this.upgrade() {
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
_ = cx.update_entity(&view, |this, cx| {
|
_ = this.update(cx, |this, cx| {
|
||||||
cx.update_entity(&this.state, |model, cx| {
|
let message = Message::new(
|
||||||
let message = Message::new(
|
this.owner.clone(),
|
||||||
owner,
|
content.to_string().into(),
|
||||||
content.to_string().into(),
|
message_time(Timestamp::now()).into(),
|
||||||
message_time(Timestamp::now()).into(),
|
);
|
||||||
);
|
|
||||||
|
|
||||||
model.items.extend(vec![message]);
|
// Update message list
|
||||||
model.count = model.items.len();
|
cx.update_entity(&this.state, |this, cx| {
|
||||||
|
this.items.extend(vec![message]);
|
||||||
|
this.count = this.items.len();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(input) = view.upgrade() {
|
// Reset message input
|
||||||
cx.update_window(window_handle, |_, window, cx| {
|
cx.update_entity(&this.input, |this, cx| {
|
||||||
cx.update_entity(&input, |input, cx| {
|
this.set_loading(false, window, cx);
|
||||||
input.set_loading(false, window, cx);
|
this.set_disabled(false, window, cx);
|
||||||
input.set_disabled(false, window, cx);
|
this.set_text("", window, cx);
|
||||||
input.set_text("", window, cx);
|
cx.notify();
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
.unwrap()
|
});
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upload(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let attaches = self.attaches.clone();
|
let window_handle = window.window_handle();
|
||||||
|
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
files: true,
|
files: true,
|
||||||
directories: false,
|
directories: false,
|
||||||
@@ -372,7 +370,7 @@ impl Chat {
|
|||||||
self.set_loading(true, cx);
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
// TODO: support multiple upload
|
// TODO: support multiple upload
|
||||||
cx.spawn(move |this, mut async_cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||||
Ok(Some(mut paths)) => {
|
Ok(Some(mut paths)) => {
|
||||||
let path = paths.pop().unwrap();
|
let path = paths.pop().unwrap();
|
||||||
@@ -382,33 +380,39 @@ impl Chat {
|
|||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
|
|
||||||
if let Ok(url) = nip96_upload(client, file_data).await {
|
if let Ok(url) = nip96_upload(client, file_data).await {
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(url) = rx.await {
|
if let Ok(url) = rx.await {
|
||||||
// Stop loading spinner
|
_ = cx.update_window(window_handle, |_, _, cx| {
|
||||||
if let Some(view) = this.upgrade() {
|
_ = this.update(cx, |this, cx| {
|
||||||
_ = async_cx.update_entity(&view, |this, cx| {
|
// Stop loading spinner
|
||||||
this.set_loading(false, cx);
|
this.set_loading(false, cx);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update attaches model
|
this.attaches.update(cx, |this, cx| {
|
||||||
_ = async_cx.update_entity(&attaches, |model, cx| {
|
if let Some(model) = this.as_mut() {
|
||||||
if let Some(model) = model.as_mut() {
|
model.push(url);
|
||||||
model.push(url);
|
} else {
|
||||||
} else {
|
*this = Some(vec![url]);
|
||||||
*model = Some(vec![url]);
|
}
|
||||||
}
|
cx.notify();
|
||||||
cx.notify();
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {
|
||||||
|
// Stop loading spinner
|
||||||
|
if let Some(view) = this.upgrade() {
|
||||||
|
cx.update_entity(&view, |this, cx| {
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(_) => {}
|
Err(_) => {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -436,15 +440,8 @@ impl Panel for Chat {
|
|||||||
self.id.clone()
|
self.id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn panel_facepile(&self, cx: &App) -> Option<Vec<String>> {
|
fn panel_facepile(&self, _cx: &App) -> Option<Vec<String>> {
|
||||||
Some(
|
Some(self.members.iter().map(|member| member.avatar()).collect())
|
||||||
self.room
|
|
||||||
.read(cx)
|
|
||||||
.members
|
|
||||||
.iter()
|
|
||||||
.map(|member| member.avatar())
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
fn title(&self, _cx: &App) -> AnyElement {
|
||||||
@@ -561,11 +558,7 @@ impl Render for Chat {
|
|||||||
.rounded(ButtonRounded::Medium)
|
.rounded(ButtonRounded::Medium)
|
||||||
.label("SEND")
|
.label("SEND")
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.send_message(
|
this.send_message(window, cx)
|
||||||
this.input.downgrade(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use async_utility::task::spawn;
|
use async_utility::task::spawn;
|
||||||
use common::{constants::IMAGE_SERVICE, profile::NostrProfile, utils::nip96_upload};
|
use common::{constants::IMAGE_SERVICE, profile::NostrProfile, utils::nip96_upload};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten, FocusHandle,
|
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Window,
|
Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render,
|
||||||
|
SharedString, Styled, Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
|
use std::str::FromStr;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonVariants},
|
||||||
@@ -112,34 +112,39 @@ impl Profile {
|
|||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
|
|
||||||
if let Ok(url) = nip96_upload(client, file_data).await {
|
if let Ok(url) = nip96_upload(client, file_data).await {
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(url) = rx.await {
|
if let Ok(url) = rx.await {
|
||||||
// Stop loading spinner
|
cx.update_window(window_handle, |_, window, cx| {
|
||||||
if let Some(view) = this.upgrade() {
|
// Stop loading spinner
|
||||||
cx.update_entity(&view, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_loading(false, cx);
|
this.set_loading(false, cx);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
|
||||||
|
|
||||||
// Update avatar input
|
// Set avatar input
|
||||||
if let Some(input) = avatar_input.upgrade() {
|
avatar_input
|
||||||
cx.update_window(window_handle, |_, window, cx| {
|
.update(cx, |this, cx| {
|
||||||
cx.update_entity(&input, |this, cx| {
|
|
||||||
this.set_text(url.to_string(), window, cx);
|
this.set_text(url.to_string(), window, cx);
|
||||||
});
|
})
|
||||||
})
|
.unwrap();
|
||||||
.unwrap();
|
})
|
||||||
}
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {
|
||||||
|
// Stop loading spinner
|
||||||
|
if let Some(view) = this.upgrade() {
|
||||||
|
cx.update_entity(&view, |this, cx| {
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(_) => {}
|
Err(_) => {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -189,18 +194,14 @@ impl Profile {
|
|||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
if rx.await.is_ok() {
|
if rx.await.is_ok() {
|
||||||
if let Some(profile) = this.upgrade() {
|
cx.update_window(window_handle, |_, window, cx| {
|
||||||
cx.update_window(window_handle, |_, window, cx| {
|
this.update(cx, |this, cx| {
|
||||||
cx.update_entity(&profile, |this, cx| {
|
this.set_submitting(false, cx);
|
||||||
this.set_submitting(false, cx);
|
window.push_notification("Your profile has been updated successfully", cx);
|
||||||
window.push_notification(
|
|
||||||
"Your profile has been updated successfully",
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap()
|
||||||
}
|
})
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -263,16 +264,29 @@ impl Render for Profile {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h_24()
|
.h_24()
|
||||||
.child(
|
.map(|this| {
|
||||||
img(format!(
|
let picture = self.avatar_input.read(cx).text();
|
||||||
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1",
|
|
||||||
IMAGE_SERVICE,
|
if picture.is_empty() {
|
||||||
self.avatar_input.read(cx).text()
|
this.child(
|
||||||
))
|
img("brand/avatar.png")
|
||||||
.size_10()
|
.size_10()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.flex_shrink_0(),
|
.flex_shrink_0(),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
this.child(
|
||||||
|
img(format!(
|
||||||
|
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1",
|
||||||
|
IMAGE_SERVICE,
|
||||||
|
self.avatar_input.read(cx).text()
|
||||||
|
))
|
||||||
|
.size_10()
|
||||||
|
.rounded_full()
|
||||||
|
.flex_shrink_0(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
use app_state::registry::AppRegistry;
|
use app_state::registry::AppRegistry;
|
||||||
use chat_state::room::Room;
|
use chat_state::registry::ChatRegistry;
|
||||||
use common::{
|
use common::{
|
||||||
|
constants::FAKE_SIG,
|
||||||
profile::NostrProfile,
|
profile::NostrProfile,
|
||||||
utils::{random_name, room_hash},
|
utils::{random_name, signer_public_key},
|
||||||
};
|
};
|
||||||
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, BorrowAppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||||
Render, SharedString, StatefulInteractiveElement, Styled, TextAlign, Window,
|
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextAlign, Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::{collections::HashSet, time::Duration};
|
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonRounded},
|
button::{Button, ButtonRounded},
|
||||||
indicator::Indicator,
|
|
||||||
input::{InputEvent, TextInput},
|
input::{InputEvent, TextInput},
|
||||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||||
Icon, IconName, Sizable, Size, StyledExt,
|
ContextModal, Icon, IconName, Sizable, Size, StyledExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||||
@@ -31,15 +31,16 @@ pub struct Compose {
|
|||||||
title_input: Entity<TextInput>,
|
title_input: Entity<TextInput>,
|
||||||
message_input: Entity<TextInput>,
|
message_input: Entity<TextInput>,
|
||||||
user_input: Entity<TextInput>,
|
user_input: Entity<TextInput>,
|
||||||
contacts: Entity<Option<Vec<NostrProfile>>>,
|
contacts: Entity<Vec<NostrProfile>>,
|
||||||
selected: Entity<HashSet<PublicKey>>,
|
selected: Entity<HashSet<PublicKey>>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
is_loading: bool,
|
is_loading: bool,
|
||||||
|
is_submitting: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Compose {
|
impl Compose {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
|
||||||
let contacts = cx.new(|_| None);
|
let contacts = cx.new(|_| Vec::with_capacity(200));
|
||||||
let selected = cx.new(|_| HashSet::new());
|
let selected = cx.new(|_| HashSet::new());
|
||||||
|
|
||||||
let user_input = cx.new(|cx| {
|
let user_input = cx.new(|cx| {
|
||||||
@@ -64,9 +65,10 @@ impl Compose {
|
|||||||
TextInput::new(window, cx)
|
TextInput::new(window, cx)
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
.text_size(Size::XSmall)
|
.text_size(Size::XSmall)
|
||||||
.placeholder("Hello... (Optional)")
|
.placeholder("Hello...")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle Enter event for message input
|
||||||
cx.subscribe_in(
|
cx.subscribe_in(
|
||||||
&user_input,
|
&user_input,
|
||||||
window,
|
window,
|
||||||
@@ -79,23 +81,22 @@ impl Compose {
|
|||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let client = get_client();
|
|
||||||
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
|
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
|
||||||
|
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
let signer = client.signer().await.unwrap();
|
let client = get_client();
|
||||||
let public_key = signer.get_public_key().await.unwrap();
|
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();
|
||||||
|
|
||||||
if let Ok(profiles) = client.database().contacts(public_key).await {
|
_ = tx.send(members);
|
||||||
let members: Vec<NostrProfile> = profiles
|
}
|
||||||
.into_iter()
|
|
||||||
.map(|profile| {
|
|
||||||
NostrProfile::new(profile.public_key(), profile.metadata())
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
_ = tx.send(members);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -104,7 +105,7 @@ impl Compose {
|
|||||||
if let Some(view) = this.upgrade() {
|
if let Some(view) = this.upgrade() {
|
||||||
_ = cx.update_entity(&view, |this, cx| {
|
_ = cx.update_entity(&view, |this, cx| {
|
||||||
this.contacts.update(cx, |this, cx| {
|
this.contacts.update(cx, |this, cx| {
|
||||||
*this = Some(contacts);
|
this.extend(contacts);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -121,43 +122,116 @@ impl Compose {
|
|||||||
contacts,
|
contacts,
|
||||||
selected,
|
selected,
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
|
is_submitting: false,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn room(&self, _window: &Window, cx: &App) -> Option<Room> {
|
pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(current_user) = cx.global::<AppRegistry>().user() {
|
let selected = self.selected.read(cx).to_owned();
|
||||||
// Convert selected pubkeys into nostr tags
|
let message = self.message_input.read(cx).text();
|
||||||
let tags: Vec<Tag> = self
|
|
||||||
.selected
|
|
||||||
.read(cx)
|
|
||||||
.iter()
|
|
||||||
.map(|pk| Tag::public_key(*pk))
|
|
||||||
.collect();
|
|
||||||
let tags = Tags::new(tags);
|
|
||||||
|
|
||||||
// Convert selected pubkeys into members
|
if selected.is_empty() {
|
||||||
let members: Vec<NostrProfile> = self
|
window.push_notification("You need to add at least 1 receiver", cx);
|
||||||
.selected
|
return;
|
||||||
.read(cx)
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|pk| NostrProfile::new(pk, Metadata::new()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Get room's id
|
|
||||||
let id = room_hash(&tags);
|
|
||||||
|
|
||||||
// Get room's owner (current user)
|
|
||||||
let owner = NostrProfile::new(current_user.public_key(), Metadata::new());
|
|
||||||
|
|
||||||
// Get room's title
|
|
||||||
let title = self.title_input.read(cx).text().to_string().into();
|
|
||||||
|
|
||||||
Some(Room::new(id, owner, members, Some(title), Timestamp::now()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if message.is_empty() {
|
||||||
|
window.push_notification("Message is required", cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_user = if let Some(profile) = cx.global::<AppRegistry>().user() {
|
||||||
|
profile
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show loading spinner
|
||||||
|
self.set_submitting(true, cx);
|
||||||
|
|
||||||
|
// Get nostr client
|
||||||
|
let client = get_client();
|
||||||
|
|
||||||
|
// Get message from user's input
|
||||||
|
let content = message.to_string();
|
||||||
|
|
||||||
|
// Get room title from user's input
|
||||||
|
let title = Tag::title(self.title_input.read(cx).text().to_string());
|
||||||
|
|
||||||
|
// Get all pubkeys
|
||||||
|
let current_user = current_user.public_key();
|
||||||
|
let mut pubkeys: Vec<PublicKey> = selected.iter().copied().collect();
|
||||||
|
pubkeys.push(current_user);
|
||||||
|
|
||||||
|
// 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 tags = Tags::new(tag_list);
|
||||||
|
let window_handle = window.window_handle();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let (tx, rx) = oneshot::channel::<Event>();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let mut event: Option<Event> = None;
|
||||||
|
|
||||||
|
for pubkey in pubkeys.iter() {
|
||||||
|
if let Ok(output) = client
|
||||||
|
.send_private_msg(*pubkey, &content, tags.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if pubkey == ¤t_user && 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| {
|
||||||
|
cx.update_global::<ChatRegistry, _>(|this, cx| {
|
||||||
|
this.new_room_message(event, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop loading spinner
|
||||||
|
_ = this.update(cx, |this, cx| {
|
||||||
|
this.set_submitting(false, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
window.close_modal(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn label(&self, _window: &Window, cx: &App) -> SharedString {
|
pub fn label(&self, _window: &Window, cx: &App) -> SharedString {
|
||||||
@@ -168,35 +242,47 @@ impl Compose {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_submitting(&self) -> bool {
|
||||||
|
self.is_submitting
|
||||||
|
}
|
||||||
|
|
||||||
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
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();
|
||||||
let input = self.user_input.downgrade();
|
|
||||||
|
|
||||||
// Show loading spinner
|
// Show loading spinner
|
||||||
self.set_loading(true, cx);
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
if let Ok(public_key) = PublicKey::parse(&content) {
|
if let Ok(public_key) = PublicKey::parse(&content) {
|
||||||
cx.spawn(|this, mut async_cx| async move {
|
if self
|
||||||
let query: anyhow::Result<Metadata, anyhow::Error> = async_cx
|
.contacts
|
||||||
.background_executor()
|
.read(cx)
|
||||||
.spawn(async move {
|
.iter()
|
||||||
let client = get_client();
|
.any(|c| c.public_key() == public_key)
|
||||||
let metadata = client
|
{
|
||||||
.fetch_metadata(public_key, Duration::from_secs(3))
|
self.set_loading(false, cx);
|
||||||
.await?;
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
Ok(metadata)
|
cx.spawn(|this, mut cx| async move {
|
||||||
})
|
let (tx, rx) = oneshot::channel::<Metadata>();
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(metadata) = query {
|
cx.background_spawn(async move {
|
||||||
if let Some(view) = this.upgrade() {
|
let client = get_client();
|
||||||
_ = async_cx.update_entity(&view, |this, cx| {
|
let metadata = (client
|
||||||
|
.fetch_metadata(public_key, Duration::from_secs(3))
|
||||||
|
.await)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
_ = tx.send(metadata);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
if let Ok(metadata) = rx.await {
|
||||||
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
|
_ = this.update(cx, |this, cx| {
|
||||||
this.contacts.update(cx, |this, cx| {
|
this.contacts.update(cx, |this, cx| {
|
||||||
if let Some(members) = this {
|
this.insert(0, NostrProfile::new(public_key, metadata));
|
||||||
members.insert(0, NostrProfile::new(public_key, metadata));
|
|
||||||
}
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,22 +291,22 @@ impl Compose {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Stop loading indicator
|
||||||
this.set_loading(false, cx);
|
this.set_loading(false, cx);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(input) = input.upgrade() {
|
// Clear input
|
||||||
_ = async_cx.update_window(window_handle, |_, window, cx| {
|
this.user_input.update(cx, |this, cx| {
|
||||||
cx.update_entity(&input, |this, cx| {
|
|
||||||
this.set_text("", window, cx);
|
this.set_text("", window, cx);
|
||||||
})
|
cx.notify();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
} else {
|
} else {
|
||||||
// Handle error
|
self.set_loading(false, cx);
|
||||||
|
window.push_notification("Public Key is not valid", cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +315,11 @@ impl Compose {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.is_submitting = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn on_action_select(
|
fn on_action_select(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: &SelectContact,
|
action: &SelectContact,
|
||||||
@@ -318,114 +409,103 @@ impl Render for Compose {
|
|||||||
.child(self.user_input.clone()),
|
.child(self.user_input.clone()),
|
||||||
)
|
)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(contacts) = self.contacts.read(cx).clone() {
|
let contacts = self.contacts.read(cx).clone();
|
||||||
let view = cx.entity();
|
let view = cx.entity();
|
||||||
let total = contacts.len();
|
|
||||||
|
|
||||||
if total != 0 {
|
if contacts.is_empty() {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h_24()
|
.h_24()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_align(TextAlign::Center)
|
.text_align(TextAlign::Center)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.line_height(relative(1.2))
|
.line_height(relative(1.2))
|
||||||
.child("No contacts"),
|
.child("No contacts"),
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(
|
|
||||||
cx.theme()
|
|
||||||
.base
|
|
||||||
.step(cx, ColorScaleStep::ELEVEN),
|
|
||||||
)
|
|
||||||
.child("Your recently contacts will appear here."),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(
|
|
||||||
uniform_list(
|
|
||||||
view,
|
|
||||||
"contacts",
|
|
||||||
total,
|
|
||||||
move |this, range, _window, cx| {
|
|
||||||
let selected = this.selected.read(cx);
|
|
||||||
let mut items = Vec::new();
|
|
||||||
|
|
||||||
for ix in range {
|
|
||||||
let item = contacts.get(ix).unwrap().clone();
|
|
||||||
let is_select =
|
|
||||||
selected.contains(&item.public_key());
|
|
||||||
|
|
||||||
items.push(
|
|
||||||
div()
|
|
||||||
.id(ix)
|
|
||||||
.w_full()
|
|
||||||
.h_9()
|
|
||||||
.px_2()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.text_xs()
|
|
||||||
.child(div().flex_shrink_0().child(
|
|
||||||
img(item.avatar()).size_6(),
|
|
||||||
))
|
|
||||||
.child(item.name()),
|
|
||||||
)
|
|
||||||
.when(is_select, |this| {
|
|
||||||
this.child(
|
|
||||||
Icon::new(IconName::CircleCheck)
|
|
||||||
.size_3()
|
|
||||||
.text_color(
|
|
||||||
cx.theme().base.step(
|
|
||||||
cx,
|
|
||||||
ColorScaleStep::TWELVE,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.hover(|this| {
|
|
||||||
this.bg(cx
|
|
||||||
.theme()
|
|
||||||
.base
|
|
||||||
.step(cx, ColorScaleStep::THREE))
|
|
||||||
})
|
|
||||||
.on_click(move |_, window, cx| {
|
|
||||||
window.dispatch_action(
|
|
||||||
Box::new(SelectContact(
|
|
||||||
item.public_key(),
|
|
||||||
)),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.min_h(px(300.)),
|
.child(
|
||||||
)
|
div()
|
||||||
}
|
.text_xs()
|
||||||
|
.text_color(
|
||||||
|
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
||||||
|
)
|
||||||
|
.child("Your recently contacts will appear here."),
|
||||||
|
),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
this.flex()
|
this.child(
|
||||||
.items_center()
|
uniform_list(
|
||||||
.justify_center()
|
view,
|
||||||
.h_16()
|
"contacts",
|
||||||
.child(Indicator::new().small())
|
contacts.len(),
|
||||||
|
move |this, range, _window, cx| {
|
||||||
|
let selected = this.selected.read(cx);
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
for ix in range {
|
||||||
|
let item = contacts.get(ix).unwrap().clone();
|
||||||
|
let is_select = selected.contains(&item.public_key());
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
div()
|
||||||
|
.id(ix)
|
||||||
|
.w_full()
|
||||||
|
.h_9()
|
||||||
|
.px_2()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.text_xs()
|
||||||
|
.child(
|
||||||
|
div().flex_shrink_0().child(
|
||||||
|
img(item.avatar()).size_6(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(item.name()),
|
||||||
|
)
|
||||||
|
.when(is_select, |this| {
|
||||||
|
this.child(
|
||||||
|
Icon::new(IconName::CircleCheck)
|
||||||
|
.size_3()
|
||||||
|
.text_color(cx.theme().base.step(
|
||||||
|
cx,
|
||||||
|
ColorScaleStep::TWELVE,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.hover(|this| {
|
||||||
|
this.bg(cx
|
||||||
|
.theme()
|
||||||
|
.base
|
||||||
|
.step(cx, ColorScaleStep::THREE))
|
||||||
|
})
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
window.dispatch_action(
|
||||||
|
Box::new(SelectContact(
|
||||||
|
item.public_key(),
|
||||||
|
)),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.min_h(px(250.)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ impl Inbox {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.rounded(px(cx.theme().radius))
|
.rounded(px(cx.theme().radius))
|
||||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)))
|
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)))
|
||||||
.child(div().font_medium().map(|this| {
|
.child(div().flex_1().truncate().font_medium().map(|this| {
|
||||||
if room.is_group {
|
if room.is_group {
|
||||||
this.flex()
|
this.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -113,6 +113,7 @@ impl Inbox {
|
|||||||
}))
|
}))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.flex_shrink_0()
|
||||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||||
.child(ago),
|
.child(ago),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use crate::views::sidebar::inbox::Inbox;
|
use crate::views::sidebar::inbox::Inbox;
|
||||||
use chat_state::registry::ChatRegistry;
|
|
||||||
use compose::Compose;
|
use compose::Compose;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, AnyElement, App, AppContext, BorrowAppContext, Context, Entity, EventEmitter,
|
div, px, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||||
StatefulInteractiveElement, Styled, Window,
|
StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
@@ -11,7 +10,7 @@ use ui::{
|
|||||||
dock_area::panel::{Panel, PanelEvent},
|
dock_area::panel::{Panel, PanelEvent},
|
||||||
popup_menu::PopupMenu,
|
popup_menu::PopupMenu,
|
||||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||||
v_flex, ContextModal, Icon, IconName, Sizable, StyledExt,
|
v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod compose;
|
mod compose;
|
||||||
@@ -53,6 +52,7 @@ impl Sidebar {
|
|||||||
|
|
||||||
window.open_modal(cx, move |modal, window, cx| {
|
window.open_modal(cx, move |modal, window, cx| {
|
||||||
let label = compose.read(cx).label(window, cx);
|
let label = compose.read(cx).label(window, cx);
|
||||||
|
let is_submitting = compose.read(cx).is_submitting();
|
||||||
|
|
||||||
modal
|
modal
|
||||||
.title("Direct Messages")
|
.title("Direct Messages")
|
||||||
@@ -70,14 +70,10 @@ impl Sidebar {
|
|||||||
.bold()
|
.bold()
|
||||||
.rounded(ButtonRounded::Large)
|
.rounded(ButtonRounded::Large)
|
||||||
.w_full()
|
.w_full()
|
||||||
|
.loading(is_submitting)
|
||||||
|
.disabled(is_submitting)
|
||||||
.on_click(window.listener_for(&compose, |this, _, window, cx| {
|
.on_click(window.listener_for(&compose, |this, _, window, cx| {
|
||||||
if let Some(room) = this.room(window, cx) {
|
this.compose(window, cx)
|
||||||
cx.update_global::<ChatRegistry, _>(|this, cx| {
|
|
||||||
this.new_room(room, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.close_modal(cx);
|
|
||||||
}
|
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::utils::{compare, room_hash};
|
use common::utils::{compare, room_hash};
|
||||||
use gpui::{App, AppContext, Entity, Global, WeakEntity};
|
use gpui::{App, AppContext, Entity, Global, WeakEntity, Window};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
|
|
||||||
@@ -28,8 +28,11 @@ impl ChatRegistry {
|
|||||||
let mut profiles = Vec::new();
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
for public_key in pubkeys.into_iter() {
|
for public_key in pubkeys.into_iter() {
|
||||||
let query = client.database().metadata(public_key).await?;
|
let metadata = client
|
||||||
let metadata = query.unwrap_or_default();
|
.database()
|
||||||
|
.metadata(public_key)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
profiles.push((public_key, metadata));
|
profiles.push((public_key, metadata));
|
||||||
}
|
}
|
||||||
@@ -56,14 +59,16 @@ impl ChatRegistry {
|
|||||||
cx.set_global(Self { inbox });
|
cx.set_global(Self { inbox });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(&mut self, cx: &mut App) {
|
pub fn load(&mut self, window: &mut Window, cx: &mut App) {
|
||||||
|
let window_handle = window.window_handle();
|
||||||
|
|
||||||
self.inbox.update(cx, |this, cx| {
|
self.inbox.update(cx, |this, cx| {
|
||||||
let task = this.load(cx.to_async());
|
let task = this.load(cx.to_async());
|
||||||
|
|
||||||
cx.spawn(|this, mut async_cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
if let Some(inbox) = this.upgrade() {
|
if let Ok(events) = task.await {
|
||||||
if let Ok(events) = task.await {
|
_ = cx.update_window(window_handle, |_, _, cx| {
|
||||||
_ = async_cx.update_entity(&inbox, |this, cx| {
|
_ = this.update(cx, |this, cx| {
|
||||||
let current_rooms = this.get_room_ids(cx);
|
let current_rooms = this.get_room_ids(cx);
|
||||||
let items: Vec<Entity<Room>> = events
|
let items: Vec<Entity<Room>> = events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -83,7 +88,7 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -114,29 +119,42 @@ impl ChatRegistry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_room_message(&mut self, event: Event, cx: &mut App) {
|
pub fn new_room_message(&mut self, event: Event, window: &mut Window, cx: &mut App) {
|
||||||
|
let window_handle = window.window_handle();
|
||||||
|
|
||||||
|
// Get all pubkeys from event's tags for comparision
|
||||||
let mut pubkeys: Vec<_> = event.tags.public_keys().copied().collect();
|
let mut pubkeys: Vec<_> = event.tags.public_keys().copied().collect();
|
||||||
pubkeys.push(event.pubkey);
|
pubkeys.push(event.pubkey);
|
||||||
|
|
||||||
self.inbox.update(cx, |this, cx| {
|
if let Some(room) = self
|
||||||
if let Some(room) = this.rooms.iter().find(|room| {
|
.inbox
|
||||||
let all_keys = room.read(cx).get_pubkeys();
|
.read(cx)
|
||||||
compare(&all_keys, &pubkeys)
|
.rooms
|
||||||
}) {
|
.iter()
|
||||||
room.update(cx, |this, cx| {
|
.find(|room| compare(&room.read(cx).get_pubkeys(), &pubkeys))
|
||||||
this.new_messages.push(event);
|
{
|
||||||
cx.notify();
|
let weak_room = room.downgrade();
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let room = cx.new(|_| Room::parse(&event));
|
|
||||||
|
|
||||||
self.inbox.update(cx, |this, cx| {
|
cx.spawn(|mut cx| async move {
|
||||||
this.rooms.insert(0, room);
|
if let Err(e) = cx.update_window(window_handle, |_, _, cx| {
|
||||||
cx.notify();
|
_ = weak_room.update(cx, |this, cx| {
|
||||||
})
|
this.last_seen = event.created_at;
|
||||||
}
|
this.new_messages.push(event);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
});
|
||||||
|
}) {
|
||||||
|
println!("Error: {}", e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
} else {
|
||||||
|
let room = cx.new(|_| Room::parse(&event));
|
||||||
|
|
||||||
|
self.inbox.update(cx, |this, cx| {
|
||||||
|
this.rooms.insert(0, room);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use common::{
|
|||||||
};
|
};
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
@@ -59,14 +60,19 @@ impl Room {
|
|||||||
let id = room_hash(&event.tags);
|
let id = room_hash(&event.tags);
|
||||||
let last_seen = event.created_at;
|
let last_seen = event.created_at;
|
||||||
|
|
||||||
|
// Always equal to current user
|
||||||
let owner = NostrProfile::new(event.pubkey, Metadata::default());
|
let owner = NostrProfile::new(event.pubkey, Metadata::default());
|
||||||
|
|
||||||
|
// Get all pubkeys that invole in this group
|
||||||
let members: Vec<NostrProfile> = event
|
let members: Vec<NostrProfile> = event
|
||||||
.tags
|
.tags
|
||||||
.public_keys()
|
.public_keys()
|
||||||
.copied()
|
.collect::<HashSet<_>>()
|
||||||
.map(|public_key| NostrProfile::new(public_key, Metadata::default()))
|
.into_iter()
|
||||||
|
.map(|public_key| NostrProfile::new(*public_key, Metadata::default()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Get title from event's tags
|
||||||
let title = if let Some(tag) = event.tags.find(TagKind::Title) {
|
let title = if let Some(tag) = event.tags.find(TagKind::Title) {
|
||||||
tag.content().map(|s| s.to_owned().into())
|
tag.content().map(|s| s.to_owned().into())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps";
|
|||||||
pub const IMAGE_SERVICE: &str = "https://wsrv.nl";
|
pub const IMAGE_SERVICE: &str = "https://wsrv.nl";
|
||||||
|
|
||||||
/// NIP96 Media Server
|
/// NIP96 Media Server
|
||||||
pub const NIP96_SERVER: &str = "https://nostrcheck.me";
|
pub const NIP96_SERVER: &str = "https://nostrmedia.com";
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ pub struct NostrProfile {
|
|||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<PublicKey> for NostrProfile {
|
||||||
|
fn as_ref(&self) -> &PublicKey {
|
||||||
|
&self.public_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<Metadata> for NostrProfile {
|
||||||
|
fn as_ref(&self) -> &Metadata {
|
||||||
|
&self.metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialEq for NostrProfile {
|
impl PartialEq for NostrProfile {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.public_key() == other.public_key()
|
self.public_key() == other.public_key()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::constants::NIP96_SERVER;
|
use crate::constants::NIP96_SERVER;
|
||||||
use chrono::{Datelike, Local, TimeZone};
|
use chrono::{Datelike, Local, TimeZone};
|
||||||
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use rnglib::{Language, RNG};
|
use rnglib::{Language, RNG};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -7,6 +8,13 @@ use std::{
|
|||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub async fn signer_public_key(client: &Client) -> anyhow::Result<PublicKey, anyhow::Error> {
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
Ok(public_key)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> {
|
pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> {
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let server_url = Url::parse(NIP96_SERVER)?;
|
let server_url = Url::parse(NIP96_SERVER)?;
|
||||||
@@ -18,8 +26,9 @@ pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url,
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn room_hash(tags: &Tags) -> u64 {
|
pub fn room_hash(tags: &Tags) -> u64 {
|
||||||
let pubkeys: Vec<PublicKey> = tags.public_keys().copied().collect();
|
let pubkeys: Vec<&PublicKey> = tags.public_keys().unique_by(|&pubkey| pubkey).collect();
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
|
|
||||||
// Generate unique hash
|
// Generate unique hash
|
||||||
pubkeys.hash(&mut hasher);
|
pubkeys.hash(&mut hasher);
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ impl TextElement {
|
|||||||
// cursor blink
|
// cursor blink
|
||||||
let cursor_height =
|
let cursor_height =
|
||||||
window.text_style().font_size.to_pixels(window.rem_size()) + px(2.);
|
window.text_style().font_size.to_pixels(window.rem_size()) + px(2.);
|
||||||
|
|
||||||
cursor = Some(fill(
|
cursor = Some(fill(
|
||||||
Bounds::new(
|
Bounds::new(
|
||||||
point(
|
point(
|
||||||
@@ -148,7 +149,7 @@ impl TextElement {
|
|||||||
),
|
),
|
||||||
size(px(1.), cursor_height),
|
size(px(1.), cursor_height),
|
||||||
),
|
),
|
||||||
cx.theme().accent.step(cx, ColorScaleStep::NINE),
|
cx.theme().accent.step(cx, ColorScaleStep::TEN),
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user