diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index fcb93b9..fc66711 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -7,16 +7,16 @@ use global::{nostr_client, sent_ids}; use gpui::prelude::FluentBuilder; use gpui::{ div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext, - ClipboardItem, Context, Div, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, + ClipboardItem, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, MouseButton, ObjectFit, - ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, Stateful, + ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window, }; use gpui_tokio::Tokio; use i18n::{shared_t, t}; use itertools::Itertools; use nostr_sdk::prelude::*; -use registry::message::RenderedMessage; +use registry::message::{Message, RenderedMessage}; use registry::room::{Room, RoomKind, RoomSignal, SendReport}; use registry::Registry; use serde::Deserialize; @@ -48,23 +48,25 @@ pub fn init(room: Entity, window: &mut Window, cx: &mut App) -> Entity, list_state: ListState, - messages: Vec, + messages: Vec, rendered_texts_by_id: HashMap, reports_by_id: HashMap>, + // New Message input: Entity, replies_to: Entity>, sending: bool, + // Media Attachment attachments: Entity>, uploading: bool, - // System + + // Panel + id: SharedString, + focus_handle: FocusHandle, image_cache: Entity, _subscriptions: SmallVec<[Subscription; 2]>, @@ -73,10 +75,7 @@ pub struct Chat { impl Chat { pub fn new(room: Entity, window: &mut Window, cx: &mut Context) -> Self { - let attachments = cx.new(|_| vec![]); - let replies_to = cx.new(|_| vec![]); let list_state = ListState::new(1, ListAlignment::Bottom, px(1024.)); - let input = cx.new(|cx| { InputState::new(window, cx) .placeholder(t!("chat.placeholder")) @@ -88,6 +87,8 @@ impl Chat { .clean_on_escape() }); + let attachments = cx.new(|_| vec![]); + let replies_to = cx.new(|_| vec![]); let load_messages = room.read(cx).load_messages(cx); let mut subscriptions = smallvec![]; @@ -154,7 +155,7 @@ impl Chat { focus_handle: cx.focus_handle(), uploading: false, sending: false, - messages: Vec::new(), + messages: vec![Message::System], rendered_texts_by_id: HashMap::new(), reports_by_id: HashMap::new(), room, @@ -329,9 +330,17 @@ impl Chat { /// Get a message by its ID fn message(&self, id: &EventId) -> Option<&RenderedMessage> { - self.messages.iter().find(|m| m.id == *id) + self.messages.iter().find_map(|msg| { + if let Message::User(rendered) = msg { + if &rendered.id == id { + return Some(rendered); + } + } + None + }) } + /// Convert and insert a nostr event into the chat panel fn insert_message(&mut self, event: E, cx: &mut Context) where E: Into, @@ -340,7 +349,7 @@ impl Chat { let new_len = 1; // Extend the messages list with the new events - self.messages.push(event.into()); + self.messages.push(Message::user(event)); // Update list state with the new messages self.list_state.splice(old_len..old_len, new_len); @@ -348,25 +357,42 @@ impl Chat { cx.notify(); } + /// Convert and insert bulk nostr events into the chat panel fn insert_messages(&mut self, events: E, cx: &mut Context) where E: IntoIterator, E::Item: Into, { - let old_len = self.messages.len(); - let old_events: HashSet = self.messages.iter().map(|msg| msg.id).collect(); - - let events: Vec = events - .into_iter() - .map(Into::into) - .filter(|msg| !old_events.contains(&msg.id)) + let old_events: HashSet = self + .messages + .iter() + .filter_map(|msg| { + if let Message::User(rendered) = msg { + Some(rendered.id) + } else { + None + } + }) .collect(); + let events: Vec = events + .into_iter() + .map(|ev| ev.into()) + .filter(|msg: &RenderedMessage| !old_events.contains(&msg.id)) + .map(Message::User) + .collect(); + + let old_len = self.messages.len(); let new_len = events.len(); // Extend the messages list with the new events self.messages.extend(events); - self.messages.sort_by_key(|m| m.created_at); + self.messages.sort_by(|a, b| match (a, b) { + (Message::System, Message::System) => std::cmp::Ordering::Equal, + (Message::System, Message::User(_)) => std::cmp::Ordering::Less, + (Message::User(_), Message::System) => std::cmp::Ordering::Greater, + (Message::User(a_msg), Message::User(b_msg)) => a_msg.created_at.cmp(&b_msg.created_at), + }); // Update list state with the new messages self.list_state.splice(old_len..old_len, new_len); @@ -380,7 +406,13 @@ impl Chat { } fn scroll_to(&self, id: EventId) { - if let Some(ix) = self.messages.iter().position(|m| m.id == id) { + if let Some(ix) = self.messages.iter().position(|m| { + if let Message::User(msg) = m { + msg.id == id + } else { + false + } + }) { self.list_state.scroll_to_reveal_item(ix); } } @@ -503,8 +535,7 @@ impl Chat { cx.notify(); } - #[allow(dead_code)] - fn render_announcement(&mut self, ix: usize, cx: &mut Context) -> Stateful
{ + fn render_announcement(&mut self, ix: usize, cx: &mut Context) -> AnyElement { v_flex() .id(ix) .group("") @@ -527,18 +558,30 @@ impl Chat { .text_color(cx.theme().elevated_surface_background), ) .child(shared_t!("chat.notice")) + .into_any_element() + } + + fn render_message_not_found(&self, cx: &Context) -> AnyElement { + div() + .w_full() + .py_1() + .px_3() + .child( + div() + .text_xs() + .text_color(cx.theme().danger_foreground) + .child(shared_t!("chat.not_found")), + ) + .into_any_element() } fn render_message( - &mut self, + &self, ix: usize, - window: &mut Window, - cx: &mut Context, - ) -> Stateful
{ - let Some(message) = self.messages.get(ix) else { - return div().id(ix); - }; - + message: &RenderedMessage, + text: AnyElement, + cx: &Context, + ) -> AnyElement { let proxy = AppSettings::get_proxy_user_avatars(cx); let hide_avatar = AppSettings::get_hide_user_avatars(cx); @@ -554,13 +597,6 @@ impl Chat { // Check if message is sent successfully let is_sent_success = self.is_sent_success(&id); - // Get or insert rendered text - let rendered_text = self - .rendered_texts_by_id - .entry(id) - .or_insert_with(|| RenderedText::new(&message.content, cx)) - .element(ix.into(), window, cx); - div() .id(ix) .group("") @@ -604,7 +640,7 @@ impl Chat { .when(has_replies, |this| { this.children(self.render_message_replies(replies, cx)) }) - .child(rendered_text) + .child(text) .when(is_sent_failed, |this| { this.child(self.render_message_reports(&id, cx)) }), @@ -624,6 +660,7 @@ impl Chat { } })) .hover(|this| this.bg(cx.theme().surface_background)) + .into_any_element() } fn render_message_replies( @@ -1135,8 +1172,23 @@ impl Render for Chat { .child( list( self.list_state.clone(), - cx.processor(move |this, ix, window, cx| { - this.render_message(ix, window, cx).into_any_element() + cx.processor(move |this, ix: usize, window, cx| { + if let Some(message) = this.messages.get(ix) { + match message { + Message::User(rendered) => { + let text = this + .rendered_texts_by_id + .entry(rendered.id) + .or_insert_with(|| RenderedText::new(&rendered.content, cx)) + .element(ix.into(), window, cx); + + this.render_message(ix, rendered, text, cx) + } + Message::System => this.render_announcement(ix, cx), + } + } else { + this.render_message_not_found(cx) + } }), ) .flex_1(), diff --git a/crates/coop/src/views/sidebar/list_item.rs b/crates/coop/src/views/sidebar/list_item.rs index d42c541..8bbb0cb 100644 --- a/crates/coop/src/views/sidebar/list_item.rs +++ b/crates/coop/src/views/sidebar/list_item.rs @@ -15,7 +15,6 @@ use ui::actions::OpenProfile; use ui::avatar::Avatar; use ui::context_menu::ContextMenuExt; use ui::modal::ModalButtonProps; -use ui::skeleton::Skeleton; use ui::{h_flex, ContextModal, StyledExt}; use crate::views::screening; @@ -109,21 +108,7 @@ impl RenderOnce for RoomListItem { self.handler, ) else { - return h_flex() - .id(self.ix) - .h_9() - .w_full() - .px_1p5() - .gap_2() - .child(Skeleton::new().flex_shrink_0().size_6().rounded_full()) - .child( - div() - .flex_1() - .flex() - .justify_between() - .child(Skeleton::new().w_32().h_2p5().rounded_sm()) - .child(Skeleton::new().w_6().h_2p5().rounded_sm()), - ); + return div().id(self.ix); }; h_flex() diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index f23f563..6b56835 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -33,7 +33,6 @@ mod list_item; const FIND_DELAY: u64 = 600; const FIND_LIMIT: usize = 10; -const TOTAL_SKELETONS: usize = 3; pub fn init(window: &mut Window, cx: &mut App) -> Entity { Sidebar::new(window, cx) @@ -595,7 +594,6 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let registry = Registry::read_global(cx); - let loading = registry.loading; // Get rooms from either search results or the chat registry let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { @@ -611,15 +609,6 @@ impl Render for Sidebar { } }; - // Get total rooms count - let mut total_rooms = rooms.len(); - - // If loading in progress - // Add 3 skeletons to the room list - if loading { - total_rooms += TOTAL_SKELETONS; - } - v_flex() .image_cache(self.image_cache.clone()) .size_full() @@ -707,7 +696,7 @@ impl Render for Sidebar { .child( uniform_list( "rooms", - total_rooms, + rooms.len(), cx.processor(move |this, range, _window, cx| { this.list_items(&rooms, range, cx) }), diff --git a/crates/registry/src/message.rs b/crates/registry/src/message.rs index 1552bcc..c27e76c 100644 --- a/crates/registry/src/message.rs +++ b/crates/registry/src/message.rs @@ -2,6 +2,18 @@ use std::hash::Hash; use nostr_sdk::prelude::*; +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum Message { + User(RenderedMessage), + System, +} + +impl Message { + pub fn user(user: impl Into) -> Self { + Self::User(user.into()) + } +} + #[derive(Debug, Clone)] pub struct RenderedMessage { pub id: EventId, diff --git a/locales/app.yml b/locales/app.yml index dadf71f..d3d2045 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -325,6 +325,8 @@ chat: en: "This conversation is private. Only members can see each other's messages." placeholder: en: "Message..." + not_found: + en: "Something is wrong. Coop cannot display this message" empty_message_error: en: "Cannot send an empty message" copy_message_button: