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