From d8edac0bb92c7a443b55edd28c825834a5093f05 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:16:36 +0700 Subject: [PATCH] chore: fix rooms out of order while loading (#139) * fix room out of order while loading * . * . --- Cargo.lock | 28 ++++-- crates/coop/Cargo.toml | 1 + crates/coop/src/views/chat/mod.rs | 147 ++++++++++++------------------ crates/global/src/lib.rs | 13 ++- crates/registry/Cargo.toml | 1 - crates/registry/src/lib.rs | 61 ++++++++----- crates/registry/src/message.rs | 25 ++++- 7 files changed, 150 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a91657..2b5307f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,12 +76,6 @@ dependencies = [ "equator", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -1255,6 +1249,7 @@ dependencies = [ "gpui", "gpui_tokio", "i18n", + "indexset", "itertools 0.13.0", "log", "nostr", @@ -2164,6 +2159,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "ftree" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae0379499242d3b9355c5069b43b9417def8c9b09903b930db1fe49318dc9e9" +dependencies = [ + "serde", +] + [[package]] name = "futf" version = "0.1.5" @@ -2637,8 +2641,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", ] @@ -3127,6 +3129,15 @@ dependencies = [ "serde", ] +[[package]] +name = "indexset" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff794cab64c942437d60272e215f923d466b23dfa6c999cdd0cafe5b6d170805" +dependencies = [ + "ftree", +] + [[package]] name = "inout" version = "0.1.4" @@ -5061,7 +5072,6 @@ dependencies = [ "fuzzy-matcher", "global", "gpui", - "hashbrown 0.15.5", "itertools 0.13.0", "log", "nostr", diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index f5588a5..abd0179 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -62,3 +62,4 @@ oneshot.workspace = true webbrowser.workspace = true tracing-subscriber = { version = "0.3.18", features = ["fmt"] } +indexset = "0.12.3" diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index d3bdf20..c4a5f76 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -1,5 +1,3 @@ -use std::collections::{HashMap, HashSet}; - use anyhow::anyhow; use common::display::{ReadableProfile, ReadableTimestamp}; use common::nip96::nip96_upload; @@ -14,6 +12,7 @@ use gpui::{ }; use gpui_tokio::Tokio; use i18n::{shared_t, t}; +use indexset::{BTreeMap, BTreeSet}; use itertools::Itertools; use nostr_sdk::prelude::*; use registry::message::{Message, RenderedMessage}; @@ -51,14 +50,13 @@ pub struct Chat { // Chat Room room: Entity, list_state: ListState, - messages: Vec, - rendered_texts_by_id: HashMap, - reports_by_id: HashMap>, + messages: BTreeSet, + rendered_texts_by_id: BTreeMap, + reports_by_id: BTreeMap>, // New Message input: Entity, replies_to: Entity>, - sending: bool, // Media Attachment attachments: Entity>, @@ -75,7 +73,8 @@ pub struct Chat { impl Chat { pub fn new(room: Entity, window: &mut Window, cx: &mut Context) -> Self { - let list_state = ListState::new(1, ListAlignment::Bottom, px(1024.)); + let attachments = cx.new(|_| vec![]); + let replies_to = cx.new(|_| vec![]); let input = cx.new(|cx| { InputState::new(window, cx) .placeholder(t!("chat.placeholder")) @@ -87,8 +86,9 @@ impl Chat { .clean_on_escape() }); - let attachments = cx.new(|_| vec![]); - let replies_to = cx.new(|_| vec![]); + let messages = BTreeSet::from([Message::system()]); + let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.)); + let load_messages = room.read(cx).load_messages(cx); let mut subscriptions = smallvec![]; @@ -154,10 +154,9 @@ impl Chat { image_cache: RetainAllImageCache::new(cx), focus_handle: cx.focus_handle(), uploading: false, - sending: false, - messages: vec![Message::System], - rendered_texts_by_id: HashMap::new(), - reports_by_id: HashMap::new(), + rendered_texts_by_id: BTreeMap::new(), + reports_by_id: BTreeMap::new(), + messages, room, list_state, input, @@ -223,35 +222,26 @@ impl Chat { css().sent_ids.read_blocking().contains(gift_wrap_id) } - /// Set the sending state of the chat panel - fn set_sending(&mut self, sending: bool, cx: &mut Context) { - self.sending = sending; - cx.notify(); - } - /// Send a message to all members of the chat fn send_message(&mut self, window: &mut Window, cx: &mut Context) { // Get the message which includes all attachments let content = self.input_content(cx); - // Get the backup setting - let backup = AppSettings::get_backup_messages(cx); - // Return if message is empty if content.trim().is_empty() { window.push_notification(t!("chat.empty_message_error"), cx); return; } - // Mark sending in progress - self.set_sending(true, cx); - // Temporary disable input self.input.update(cx, |this, cx| { this.set_loading(true, cx); this.set_disabled(true, cx); }); + // Get the backup setting + let backup = AppSettings::get_backup_messages(cx); + // Get replies_to if it's present let replies = self.replies_to.read(cx).clone(); @@ -266,17 +256,23 @@ impl Chat { // Create a task for sending the message in the background let send_message = room.send_in_background(&content, replies, backup, cx); - // Optimistically update message list - self.insert_message(temp_message, cx); + cx.defer_in(window, |this, window, cx| { + // Optimistically update message list + this.insert_message(temp_message, cx); - // Remove all replies - self.remove_all_replies(cx); + // Scroll to reveal the new message + this.list_state + .scroll_to_reveal_item(this.messages.len() + 1); - // Reset the input state - self.input.update(cx, |this, cx| { - this.set_loading(false, cx); - this.set_disabled(false, cx); - this.set_value("", window, cx); + // Remove all replies + this.remove_all_replies(cx); + + // Reset the input state + this.input.update(cx, |this, cx| { + this.set_loading(false, cx); + this.set_disabled(false, cx); + this.set_value("", window, cx); + }); }); // Continue sending the message in the background @@ -284,15 +280,20 @@ impl Chat { match send_message.await { Ok(reports) => { this.update(cx, |this, cx| { - // Don't change the room kind if send failed this.room.update(cx, |this, cx| { if this.kind != RoomKind::Ongoing { - this.kind = RoomKind::Ongoing; - cx.notify(); + // Update the room kind to ongoing + // But keep the room kind if send failed + if reports.iter().all(|r| !r.is_sent_success()) { + this.kind = RoomKind::Ongoing; + cx.notify(); + } } }); + + // Insert the sent reports this.reports_by_id.insert(temp_id, reports); - this.sending = false; + cx.notify(); }) .ok(); @@ -340,62 +341,23 @@ impl Chat { } /// Convert and insert a nostr event into the chat panel - fn insert_message(&mut self, event: E, cx: &mut Context) + fn insert_message(&mut self, event: E, _cx: &mut Context) where E: Into, { let old_len = self.messages.len(); - let new_len = 1; // Extend the messages list with the new events - self.messages.push(Message::user(event)); - - // Update list state with the new messages - self.list_state.splice(old_len..old_len, new_len); - - cx.notify(); + if self.messages.insert(Message::user(event)) { + self.list_state.splice(old_len..old_len, 1); + } } - /// 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_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(|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); - + /// Convert and insert a vector of nostr events into the chat panel + fn insert_messages(&mut self, events: Vec, cx: &mut Context) { + for event in events.into_iter() { + self.insert_message(event, cx); + } cx.notify(); } @@ -560,15 +522,18 @@ impl Chat { .into_any_element() } - fn render_message_not_found(&self, cx: &Context) -> AnyElement { + fn render_message_not_found(&self, ix: usize, cx: &Context) -> AnyElement { div() + .id(ix) .w_full() .py_1() .px_3() .child( - div() + h_flex() + .gap_1() .text_xs() .text_color(cx.theme().danger_foreground) + .child(SharedString::from(ix.to_string())) .child(shared_t!("chat.not_found")), ) .into_any_element() @@ -1172,7 +1137,7 @@ impl Render for Chat { list( self.list_state.clone(), cx.processor(move |this, ix: usize, window, cx| { - if let Some(message) = this.messages.get(ix) { + if let Some(message) = this.messages.get_index(ix) { match message { Message::User(rendered) => { let text = this @@ -1183,10 +1148,10 @@ impl Render for Chat { this.render_message(ix, rendered, text, cx) } - Message::System => this.render_announcement(ix, cx), + Message::System(_) => this.render_announcement(ix, cx), } } else { - this.render_message_not_found(cx) + this.render_message_not_found(ix, cx) } }), ) diff --git a/crates/global/src/lib.rs b/crates/global/src/lib.rs index 0576a30..d215557 100644 --- a/crates/global/src/lib.rs +++ b/crates/global/src/lib.rs @@ -117,6 +117,17 @@ pub struct CoopSimpleStorage { pub resend_queue: RwLock>, } +impl CoopSimpleStorage { + pub fn new() -> Self { + Self { + init_at: Timestamp::now(), + sent_ids: RwLock::new(HashSet::new()), + resent_ids: RwLock::new(Vec::new()), + resend_queue: RwLock::new(HashMap::new()), + } + } +} + static NOSTR_CLIENT: OnceLock = OnceLock::new(); static INGESTER: OnceLock = OnceLock::new(); static COOP_SIMPLE_STORAGE: OnceLock = OnceLock::new(); @@ -150,7 +161,7 @@ pub fn ingester() -> &'static Ingester { } pub fn css() -> &'static CoopSimpleStorage { - COOP_SIMPLE_STORAGE.get_or_init(CoopSimpleStorage::default) + COOP_SIMPLE_STORAGE.get_or_init(CoopSimpleStorage::new) } pub fn first_run() -> &'static bool { diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index cd105e2..87b4c92 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -19,4 +19,3 @@ smol.workspace = true log.workspace = true fuzzy-matcher = "0.3.7" -hashbrown = "0.15" diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs index 499f098..faf79b7 100644 --- a/crates/registry/src/lib.rs +++ b/crates/registry/src/lib.rs @@ -1,4 +1,5 @@ use std::cmp::Reverse; +use std::collections::{HashMap, HashSet}; use anyhow::Error; use common::event::EventUtils; @@ -6,7 +7,6 @@ use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; use global::nostr_client; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window}; -use hashbrown::{HashMap, HashSet}; use itertools::Itertools; use nostr_sdk::prelude::*; use room::RoomKind; @@ -251,9 +251,15 @@ impl Registry { /// Reset the registry. pub fn reset(&mut self, cx: &mut Context) { - self.rooms = vec![]; + // Reset the loading status (default: true) self.loading = true; + + // Clear the current identity self.identity = None; + + // Clear all current rooms + self.rooms.clear(); + cx.notify(); } @@ -262,12 +268,13 @@ impl Registry { log::info!("Starting to load chat rooms..."); // Get the contact bypass setting - let contact_bypass = AppSettings::get_contact_bypass(cx); + let bypass_setting = AppSettings::get_contact_bypass(cx); let task: Task, Error>> = cx.background_spawn(async move { let client = nostr_client(); let signer = client.signer().await?; let public_key = signer.get_public_key().await?; + let contacts = client.database().contacts_public_keys(public_key).await?; // Get messages sent by the user let send = Filter::new() @@ -300,13 +307,12 @@ impl Registry { public_keys.retain(|pk| pk != &public_key); // Bypass screening flag - let mut bypass = false; + let mut bypassed = false; - // If user enabled bypass screening for contacts - // Check if room's members are in contact with current user - if contact_bypass { - let contacts = client.database().contacts_public_keys(public_key).await?; - bypass = public_keys.iter().any(|k| contacts.contains(k)); + // If the user has enabled bypass screening in settings, + // check if any of the room's members are contacts of the current user + if bypass_setting { + bypassed = public_keys.iter().any(|k| contacts.contains(k)); } // Check if the current user has sent at least one message to this room @@ -321,7 +327,7 @@ impl Registry { // Create a new room let room = Room::new(&event).rearrange_by(public_key); - if is_ongoing || bypass { + if is_ongoing || bypassed { rooms.insert(room.kind(RoomKind::Ongoing)); } else { rooms.insert(room); @@ -349,23 +355,28 @@ impl Registry { } pub(crate) fn extend_rooms(&mut self, rooms: HashSet, cx: &mut Context) { - let mut room_map: HashMap = HashMap::with_capacity(self.rooms.len()); - - for (index, room) in self.rooms.iter().enumerate() { - room_map.insert(room.read(cx).id, index); - } + let mut room_map: HashMap = self + .rooms + .iter() + .enumerate() + .map(|(idx, room)| (room.read(cx).id, idx)) + .collect(); for new_room in rooms.into_iter() { // Check if we already have a room with this ID if let Some(&index) = room_map.get(&new_room.id) { self.rooms[index].update(cx, |this, cx| { - *this = new_room; - cx.notify(); + if new_room.created_at > this.created_at { + *this = new_room; + cx.notify(); + } }); } else { - let new_index = self.rooms.len(); - room_map.insert(new_room.id, new_index); + let new_room_id = new_room.id; self.rooms.push(cx.new(|_| new_room)); + + let new_index = self.rooms.len(); + room_map.insert(new_room_id, new_index); } } } @@ -418,9 +429,13 @@ impl Registry { }; if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) { + let is_new_event = event.created_at > room.read(cx).created_at; + // Update room room.update(cx, |this, cx| { - this.created_at(event.created_at, cx); + if is_new_event { + this.created_at(event.created_at, cx); + } // Set this room is ongoing if the new message is from current user if author == identity { @@ -433,8 +448,10 @@ impl Registry { }); }); - // Re-sort the rooms registry by their created at - self.sort(cx); + // Resort all rooms in the registry by their created at (after updated) + if is_new_event { + self.sort(cx); + } } else { let room = Room::new(&event) .kind(RoomKind::default()) diff --git a/crates/registry/src/message.rs b/crates/registry/src/message.rs index c27e76c..72778aa 100644 --- a/crates/registry/src/message.rs +++ b/crates/registry/src/message.rs @@ -2,16 +2,37 @@ use std::hash::Hash; use nostr_sdk::prelude::*; -#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum Message { User(RenderedMessage), - System, + System(Timestamp), } impl Message { pub fn user(user: impl Into) -> Self { Self::User(user.into()) } + + pub fn system() -> Self { + Self::System(Timestamp::default()) + } +} + +impl Ord for Message { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (Message::User(a), Message::User(b)) => a.cmp(b), + (Message::System(a), Message::System(b)) => a.cmp(b), + (Message::User(a), Message::System(b)) => a.created_at.cmp(b), + (Message::System(a), Message::User(b)) => a.cmp(&b.created_at), + } + } +} + +impl PartialOrd for Message { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } #[derive(Debug, Clone)]