diff --git a/assets/icons/edit-fill.svg b/assets/icons/edit-fill.svg
new file mode 100644
index 0000000..e263643
--- /dev/null
+++ b/assets/icons/edit-fill.svg
@@ -0,0 +1,4 @@
+
diff --git a/crates/chats/src/lib.rs b/crates/chats/src/lib.rs
index f5999c2..40e9fce 100644
--- a/crates/chats/src/lib.rs
+++ b/crates/chats/src/lib.rs
@@ -288,19 +288,20 @@ impl ChatRegistry {
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
room.update(cx, |this, cx| {
this.created_at(event.created_at, cx);
- this.emit_message(event, window, cx);
+
+ cx.defer_in(window, |this, window, cx| {
+ this.emit_message(event, window, cx);
+ });
});
- // Re-sort rooms by last seen
- self.rooms
- .sort_by_key(|room| Reverse(room.read(cx).created_at));
+ cx.defer_in(window, |this, _, cx| {
+ this.rooms
+ .sort_by_key(|room| Reverse(room.read(cx).created_at));
+ });
} else {
- let new_room = cx.new(|_| Room::new(&event));
-
// Push the new room to the front of the list
- self.rooms.insert(0, new_room);
+ self.rooms.insert(0, cx.new(|_| Room::new(&event)));
+ cx.notify();
}
-
- cx.notify();
}
}
diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs
index 3d6c186..c867400 100644
--- a/crates/chats/src/room.rs
+++ b/crates/chats/src/room.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use account::Account;
-use anyhow::Error;
+use anyhow::{anyhow, Error};
use chrono::{Local, TimeZone};
use common::{compare, profile::SharedProfile, room_hash};
use global::get_client;
@@ -285,6 +285,17 @@ impl Room {
cx.notify();
}
+ /// Updates the subject of the room
+ ///
+ /// # Arguments
+ ///
+ /// * `subject` - The new subject to set
+ /// * `cx` - The context to notify about the update
+ pub fn subject(&mut self, subject: String, cx: &mut Context) {
+ self.subject = Some(subject.into());
+ cx.notify();
+ }
+
/// Fetches metadata for all members in the room
///
/// # Arguments
@@ -358,15 +369,21 @@ impl Room {
/// A Task that resolves to Result, Error> where the
/// strings contain error messages for any failed sends
pub fn send_message(&self, content: String, cx: &App) -> Task, Error>> {
- let client = get_client();
+ let account = Account::global(cx).read(cx);
+
+ let Some(profile) = account.profile.clone() else {
+ return Task::ready(Err(anyhow!("User is not logged in")));
+ };
+
+ let public_key = profile.public_key();
+ let subject = self.subject.clone();
let pubkeys = self.members.clone();
cx.background_spawn(async move {
- let signer = client.signer().await?;
- let public_key = signer.get_public_key().await?;
+ let client = get_client();
let mut report = vec![];
- let tags: Vec = pubkeys
+ let mut tags: Vec = pubkeys
.iter()
.filter_map(|pubkey| {
if pubkey != &public_key {
@@ -377,6 +394,12 @@ impl Room {
})
.collect();
+ if let Some(subject) = subject {
+ tags.push(Tag::from_standardized(TagStandard::Subject(
+ subject.to_string(),
+ )));
+ }
+
for pubkey in pubkeys.iter() {
if let Err(e) = client
.send_private_msg(*pubkey, &content, tags.clone())
diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs
index 01efd16..e88a70b 100644
--- a/crates/coop/src/chatspace.rs
+++ b/crates/coop/src/chatspace.rs
@@ -22,8 +22,6 @@ use crate::views::{onboarding, sidebar};
const MODAL_WIDTH: f32 = 420.;
const SIDEBAR_WIDTH: f32 = 280.;
-impl_internal_actions!(dock, [AddPanel, ToggleModal]);
-
pub fn init(window: &mut Window, cx: &mut App) -> Entity {
ChatSpace::new(window, cx)
}
@@ -58,6 +56,8 @@ pub struct ToggleModal {
pub modal: ModalKind,
}
+impl_internal_actions!(dock, [AddPanel, ToggleModal]);
+
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct AddPanel {
panel: PanelKind,
@@ -263,7 +263,7 @@ impl ChatSpace {
};
}
- fn set_center_panel(panel: P, window: &mut Window, cx: &mut App) {
+ pub(crate) fn set_center_panel(panel: P, window: &mut Window, cx: &mut App) {
if let Some(Some(root)) = window.root::() {
if let Ok(chatspace) = root.read(cx).view().clone().downcast::() {
let panel = Arc::new(panel);
diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs
index 4922599..1582bd7 100644
--- a/crates/coop/src/main.rs
+++ b/crates/coop/src/main.rs
@@ -298,11 +298,11 @@ fn main() {
// Spawn a task to handle events from nostr channel
cx.spawn_in(window, async move |_, cx| {
- let chats = cx.update(|_, cx| ChatRegistry::global(cx)).unwrap();
- let auto_updater = cx.update(|_, cx| AutoUpdater::global(cx)).unwrap();
-
while let Ok(signal) = event_rx.recv().await {
cx.update(|window, cx| {
+ let chats = ChatRegistry::global(cx);
+ let auto_updater = AutoUpdater::global(cx);
+
match signal {
Signal::Event(event) => {
chats.update(cx, |this, cx| {
diff --git a/crates/coop/src/views/chat.rs b/crates/coop/src/views/chat.rs
index 6490228..256e543 100644
--- a/crates/coop/src/views/chat.rs
+++ b/crates/coop/src/views/chat.rs
@@ -4,12 +4,14 @@ use chats::{message::RoomMessage, room::Room, ChatRegistry};
use common::{nip96_upload, profile::SharedProfile};
use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{
- 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, Window,
+ div, img, impl_internal_actions, list, prelude::FluentBuilder, px, relative, svg, white,
+ AnyElement, App, AppContext, Context, Element, Empty, Entity, EventEmitter, Flatten,
+ FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit,
+ ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement, Styled,
+ StyledImage, Subscription, Window,
};
use nostr_sdk::prelude::*;
+use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::fs;
use std::{collections::HashMap, sync::Arc};
@@ -24,7 +26,15 @@ use ui::{
v_flex, ContextModal, Disableable, Icon, IconName, Size, StyledExt,
};
+use crate::views::subject;
+
const ALERT: &str = "has not set up Messaging (DM) Relays, so they will NOT receive your messages.";
+const DESC: &str = "This conversation is private. Only members can see each other's messages.";
+
+#[derive(Clone, PartialEq, Eq, Deserialize)]
+pub struct ChangeSubject(pub String);
+
+impl_internal_actions!(chat, [ChangeSubject]);
pub fn init(id: &u64, window: &mut Window, cx: &mut App) -> Result>, Error> {
if let Some(room) = ChatRegistry::global(cx).read(cx).room(id, cx) {
@@ -99,7 +109,7 @@ impl Chat {
this.update(cx, |this, cx| {
this.render_message(ix, window, cx).into_any_element()
})
- .unwrap()
+ .unwrap_or(Empty.into_any())
}
});
@@ -216,7 +226,7 @@ impl Chat {
return;
}
- // Disable input when sending message
+ // temporarily disable message input
self.input.update(cx, |this, cx| {
this.set_loading(true, window, cx);
this.set_disabled(true, window, cx);
@@ -226,27 +236,38 @@ impl Chat {
let task = room.send_message(content, cx);
cx.spawn_in(window, async move |this, cx| {
- if let Ok(msgs) = task.await {
- cx.update(|window, cx| {
- this.update(cx, |this, cx| {
- // Reset message input
- cx.update_entity(&this.input, |this, cx| {
- this.set_loading(false, window, cx);
- this.set_disabled(false, window, cx);
- this.set_text("", window, cx);
- cx.notify();
- });
+ match task.await {
+ Ok(reports) => {
+ cx.update(|window, cx| {
+ this.update(cx, |this, cx| {
+ // Reset message input
+ this.input.update(cx, |this, cx| {
+ this.set_loading(false, window, cx);
+ this.set_disabled(false, window, cx);
+ this.set_text("", window, cx);
+ cx.notify();
+ });
+ })
+ .ok();
+
+ for item in reports.into_iter() {
+ window.push_notification(
+ Notification::error(item).title("Message Failed to Send"),
+ cx,
+ );
+ }
})
.ok();
-
- for item in msgs.into_iter() {
+ }
+ Err(e) => {
+ cx.update(|window, cx| {
window.push_notification(
- Notification::error(item).title("Message Failed to Send"),
+ Notification::error(e.to_string()).title("Message Failed to Send"),
cx,
);
- }
- })
- .ok();
+ })
+ .ok();
+ }
}
})
.detach();
@@ -265,12 +286,15 @@ impl Chat {
cx.spawn_in(window, async move |this, cx| {
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
Ok(Some(mut paths)) => {
- let path = paths.pop().unwrap();
+ let Some(path) = paths.pop() else {
+ return;
+ };
if let Ok(file_data) = fs::read(path).await {
let client = get_client();
let (tx, rx) = oneshot::channel::();
+ // spawn task via async_utility
spawn(async move {
if let Ok(url) = nip96_upload(client, file_data).await {
_ = tx.send(url);
@@ -280,7 +304,6 @@ impl Chat {
if let Ok(url) = rx.await {
cx.update(|_, cx| {
this.update(cx, |this, cx| {
- // Stop loading spinner
this.set_loading(false, cx);
this.attaches.update(cx, |this, cx| {
@@ -299,13 +322,13 @@ impl Chat {
}
}
Ok(None) => {
- // Stop loading spinner
- if let Some(view) = this.upgrade() {
- cx.update_entity(&view, |this, cx| {
+ cx.update(|_, cx| {
+ this.update(cx, |this, cx| {
this.set_loading(false, cx);
})
- .unwrap();
- }
+ .ok();
+ })
+ .ok();
}
Err(_) => {}
}
@@ -316,9 +339,10 @@ impl Chat {
fn remove_media(&mut self, url: &Url, _window: &mut Window, cx: &mut Context) {
self.attaches.update(cx, |model, cx| {
if let Some(urls) = model.as_mut() {
- let ix = urls.iter().position(|x| x == url).unwrap();
- urls.remove(ix);
- cx.notify();
+ if let Some(ix) = urls.iter().position(|x| x == url) {
+ urls.remove(ix);
+ cx.notify();
+ }
}
});
}
@@ -334,10 +358,10 @@ impl Chat {
window: &mut Window,
cx: &mut Context,
) -> impl IntoElement {
- const ROOM_DESCRIPTION: &str =
- "This conversation is private. Only members of this chat can see each other's messages.";
+ let Some(message) = self.messages.read(cx).get(ix) else {
+ return div().into_element();
+ };
- let message = self.messages.read(cx).get(ix).unwrap();
let text_data = &mut self.text_data;
div()
@@ -427,7 +451,7 @@ impl Chat {
.size_10()
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
)
- .child(ROOM_DESCRIPTION),
+ .child(DESC),
})
}
}
@@ -472,8 +496,28 @@ impl Panel for Chat {
menu.track_focus(&self.focus_handle)
}
- fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec