chore: fix rooms out of order while loading (#139)

* fix room out of order while loading

* .

* .
This commit is contained in:
reya
2025-09-03 09:16:36 +07:00
committed by GitHub
parent d392602ed6
commit d8edac0bb9
7 changed files with 150 additions and 126 deletions

28
Cargo.lock generated
View File

@@ -76,12 +76,6 @@ dependencies = [
"equator", "equator",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@@ -1255,6 +1249,7 @@ dependencies = [
"gpui", "gpui",
"gpui_tokio", "gpui_tokio",
"i18n", "i18n",
"indexset",
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
"nostr", "nostr",
@@ -2164,6 +2159,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "ftree"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae0379499242d3b9355c5069b43b9417def8c9b09903b930db1fe49318dc9e9"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "futf" name = "futf"
version = "0.1.5" version = "0.1.5"
@@ -2637,8 +2641,6 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"allocator-api2",
"equivalent",
"foldhash", "foldhash",
] ]
@@ -3127,6 +3129,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "indexset"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff794cab64c942437d60272e215f923d466b23dfa6c999cdd0cafe5b6d170805"
dependencies = [
"ftree",
]
[[package]] [[package]]
name = "inout" name = "inout"
version = "0.1.4" version = "0.1.4"
@@ -5061,7 +5072,6 @@ dependencies = [
"fuzzy-matcher", "fuzzy-matcher",
"global", "global",
"gpui", "gpui",
"hashbrown 0.15.5",
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
"nostr", "nostr",

View File

@@ -62,3 +62,4 @@ oneshot.workspace = true
webbrowser.workspace = true webbrowser.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["fmt"] } tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
indexset = "0.12.3"

View File

@@ -1,5 +1,3 @@
use std::collections::{HashMap, HashSet};
use anyhow::anyhow; use anyhow::anyhow;
use common::display::{ReadableProfile, ReadableTimestamp}; use common::display::{ReadableProfile, ReadableTimestamp};
use common::nip96::nip96_upload; use common::nip96::nip96_upload;
@@ -14,6 +12,7 @@ use gpui::{
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use indexset::{BTreeMap, BTreeSet};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::message::{Message, RenderedMessage}; use registry::message::{Message, RenderedMessage};
@@ -51,14 +50,13 @@ pub struct Chat {
// Chat Room // Chat Room
room: Entity<Room>, room: Entity<Room>,
list_state: ListState, list_state: ListState,
messages: Vec<Message>, messages: BTreeSet<Message>,
rendered_texts_by_id: HashMap<EventId, RenderedText>, rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
reports_by_id: HashMap<EventId, Vec<SendReport>>, reports_by_id: BTreeMap<EventId, Vec<SendReport>>,
// New Message // New Message
input: Entity<InputState>, input: Entity<InputState>,
replies_to: Entity<Vec<EventId>>, replies_to: Entity<Vec<EventId>>,
sending: bool,
// Media Attachment // Media Attachment
attachments: Entity<Vec<Url>>, attachments: Entity<Vec<Url>>,
@@ -75,7 +73,8 @@ pub struct Chat {
impl Chat { impl Chat {
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> 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| { let input = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.placeholder(t!("chat.placeholder")) .placeholder(t!("chat.placeholder"))
@@ -87,8 +86,9 @@ impl Chat {
.clean_on_escape() .clean_on_escape()
}); });
let attachments = cx.new(|_| vec![]); let messages = BTreeSet::from([Message::system()]);
let replies_to = cx.new(|_| vec![]); let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
let load_messages = room.read(cx).load_messages(cx); let load_messages = room.read(cx).load_messages(cx);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
@@ -154,10 +154,9 @@ impl Chat {
image_cache: RetainAllImageCache::new(cx), image_cache: RetainAllImageCache::new(cx),
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
uploading: false, uploading: false,
sending: false, rendered_texts_by_id: BTreeMap::new(),
messages: vec![Message::System], reports_by_id: BTreeMap::new(),
rendered_texts_by_id: HashMap::new(), messages,
reports_by_id: HashMap::new(),
room, room,
list_state, list_state,
input, input,
@@ -223,35 +222,26 @@ impl Chat {
css().sent_ids.read_blocking().contains(gift_wrap_id) 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>) {
self.sending = sending;
cx.notify();
}
/// Send a message to all members of the chat /// Send a message to all members of the chat
fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Get the message which includes all attachments // Get the message which includes all attachments
let content = self.input_content(cx); let content = self.input_content(cx);
// Get the backup setting
let backup = AppSettings::get_backup_messages(cx);
// Return if message is empty // Return if message is empty
if content.trim().is_empty() { if content.trim().is_empty() {
window.push_notification(t!("chat.empty_message_error"), cx); window.push_notification(t!("chat.empty_message_error"), cx);
return; return;
} }
// Mark sending in progress
self.set_sending(true, cx);
// Temporary disable input // Temporary disable input
self.input.update(cx, |this, cx| { self.input.update(cx, |this, cx| {
this.set_loading(true, cx); this.set_loading(true, cx);
this.set_disabled(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 // Get replies_to if it's present
let replies = self.replies_to.read(cx).clone(); let replies = self.replies_to.read(cx).clone();
@@ -266,33 +256,44 @@ impl Chat {
// Create a task for sending the message in the background // Create a task for sending the message in the background
let send_message = room.send_in_background(&content, replies, backup, cx); let send_message = room.send_in_background(&content, replies, backup, cx);
cx.defer_in(window, |this, window, cx| {
// Optimistically update message list // Optimistically update message list
self.insert_message(temp_message, cx); this.insert_message(temp_message, cx);
// Scroll to reveal the new message
this.list_state
.scroll_to_reveal_item(this.messages.len() + 1);
// Remove all replies // Remove all replies
self.remove_all_replies(cx); this.remove_all_replies(cx);
// Reset the input state // Reset the input state
self.input.update(cx, |this, cx| { this.input.update(cx, |this, cx| {
this.set_loading(false, cx); this.set_loading(false, cx);
this.set_disabled(false, cx); this.set_disabled(false, cx);
this.set_value("", window, cx); this.set_value("", window, cx);
}); });
});
// Continue sending the message in the background // Continue sending the message in the background
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
match send_message.await { match send_message.await {
Ok(reports) => { Ok(reports) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
// Don't change the room kind if send failed
this.room.update(cx, |this, cx| { this.room.update(cx, |this, cx| {
if this.kind != RoomKind::Ongoing { if this.kind != RoomKind::Ongoing {
// 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; this.kind = RoomKind::Ongoing;
cx.notify(); cx.notify();
} }
}
}); });
// Insert the sent reports
this.reports_by_id.insert(temp_id, reports); this.reports_by_id.insert(temp_id, reports);
this.sending = false;
cx.notify(); cx.notify();
}) })
.ok(); .ok();
@@ -340,62 +341,23 @@ impl Chat {
} }
/// Convert and insert a nostr event into the chat panel /// Convert and insert a nostr event into the chat panel
fn insert_message<E>(&mut self, event: E, cx: &mut Context<Self>) fn insert_message<E>(&mut self, event: E, _cx: &mut Context<Self>)
where where
E: Into<RenderedMessage>, E: Into<RenderedMessage>,
{ {
let old_len = self.messages.len(); let old_len = self.messages.len();
let new_len = 1;
// Extend the messages list with the new events // Extend the messages list with the new events
self.messages.push(Message::user(event)); if self.messages.insert(Message::user(event)) {
self.list_state.splice(old_len..old_len, 1);
// Update list state with the new messages }
self.list_state.splice(old_len..old_len, new_len);
cx.notify();
} }
/// Convert and insert bulk nostr events into the chat panel /// Convert and insert a vector of nostr events into the chat panel
fn insert_messages<E>(&mut self, events: E, cx: &mut Context<Self>) fn insert_messages(&mut self, events: Vec<Event>, cx: &mut Context<Self>) {
where for event in events.into_iter() {
E: IntoIterator, self.insert_message(event, cx);
E::Item: Into<RenderedMessage>,
{
let old_events: HashSet<EventId> = self
.messages
.iter()
.filter_map(|msg| {
if let Message::User(rendered) = msg {
Some(rendered.id)
} else {
None
} }
})
.collect();
let events: Vec<Message> = 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);
cx.notify(); cx.notify();
} }
@@ -560,15 +522,18 @@ impl Chat {
.into_any_element() .into_any_element()
} }
fn render_message_not_found(&self, cx: &Context<Self>) -> AnyElement { fn render_message_not_found(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
div() div()
.id(ix)
.w_full() .w_full()
.py_1() .py_1()
.px_3() .px_3()
.child( .child(
div() h_flex()
.gap_1()
.text_xs() .text_xs()
.text_color(cx.theme().danger_foreground) .text_color(cx.theme().danger_foreground)
.child(SharedString::from(ix.to_string()))
.child(shared_t!("chat.not_found")), .child(shared_t!("chat.not_found")),
) )
.into_any_element() .into_any_element()
@@ -1172,7 +1137,7 @@ impl Render for Chat {
list( list(
self.list_state.clone(), self.list_state.clone(),
cx.processor(move |this, ix: usize, window, cx| { 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 { match message {
Message::User(rendered) => { Message::User(rendered) => {
let text = this let text = this
@@ -1183,10 +1148,10 @@ impl Render for Chat {
this.render_message(ix, rendered, text, cx) this.render_message(ix, rendered, text, cx)
} }
Message::System => this.render_announcement(ix, cx), Message::System(_) => this.render_announcement(ix, cx),
} }
} else { } else {
this.render_message_not_found(cx) this.render_message_not_found(ix, cx)
} }
}), }),
) )

View File

@@ -117,6 +117,17 @@ pub struct CoopSimpleStorage {
pub resend_queue: RwLock<HashMap<EventId, RelayUrl>>, pub resend_queue: RwLock<HashMap<EventId, RelayUrl>>,
} }
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<Client> = OnceLock::new(); static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
static INGESTER: OnceLock<Ingester> = OnceLock::new(); static INGESTER: OnceLock<Ingester> = OnceLock::new();
static COOP_SIMPLE_STORAGE: OnceLock<CoopSimpleStorage> = OnceLock::new(); static COOP_SIMPLE_STORAGE: OnceLock<CoopSimpleStorage> = OnceLock::new();
@@ -150,7 +161,7 @@ pub fn ingester() -> &'static Ingester {
} }
pub fn css() -> &'static CoopSimpleStorage { 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 { pub fn first_run() -> &'static bool {

View File

@@ -19,4 +19,3 @@ smol.workspace = true
log.workspace = true log.workspace = true
fuzzy-matcher = "0.3.7" fuzzy-matcher = "0.3.7"
hashbrown = "0.15"

View File

@@ -1,4 +1,5 @@
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
use anyhow::Error; use anyhow::Error;
use common::event::EventUtils; use common::event::EventUtils;
@@ -6,7 +7,6 @@ use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use global::nostr_client; use global::nostr_client;
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window}; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window};
use hashbrown::{HashMap, HashSet};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use room::RoomKind; use room::RoomKind;
@@ -251,9 +251,15 @@ impl Registry {
/// Reset the registry. /// Reset the registry.
pub fn reset(&mut self, cx: &mut Context<Self>) { pub fn reset(&mut self, cx: &mut Context<Self>) {
self.rooms = vec![]; // Reset the loading status (default: true)
self.loading = true; self.loading = true;
// Clear the current identity
self.identity = None; self.identity = None;
// Clear all current rooms
self.rooms.clear();
cx.notify(); cx.notify();
} }
@@ -262,12 +268,13 @@ impl Registry {
log::info!("Starting to load chat rooms..."); log::info!("Starting to load chat rooms...");
// Get the contact bypass setting // Get the contact bypass setting
let contact_bypass = AppSettings::get_contact_bypass(cx); let bypass_setting = AppSettings::get_contact_bypass(cx);
let task: Task<Result<HashSet<Room>, Error>> = cx.background_spawn(async move { let task: Task<Result<HashSet<Room>, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().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 // Get messages sent by the user
let send = Filter::new() let send = Filter::new()
@@ -300,13 +307,12 @@ impl Registry {
public_keys.retain(|pk| pk != &public_key); public_keys.retain(|pk| pk != &public_key);
// Bypass screening flag // Bypass screening flag
let mut bypass = false; let mut bypassed = false;
// If user enabled bypass screening for contacts // If the user has enabled bypass screening in settings,
// Check if room's members are in contact with current user // check if any of the room's members are contacts of the current user
if contact_bypass { if bypass_setting {
let contacts = client.database().contacts_public_keys(public_key).await?; bypassed = public_keys.iter().any(|k| contacts.contains(k));
bypass = public_keys.iter().any(|k| contacts.contains(k));
} }
// Check if the current user has sent at least one message to this room // Check if the current user has sent at least one message to this room
@@ -321,7 +327,7 @@ impl Registry {
// Create a new room // Create a new room
let room = Room::new(&event).rearrange_by(public_key); let room = Room::new(&event).rearrange_by(public_key);
if is_ongoing || bypass { if is_ongoing || bypassed {
rooms.insert(room.kind(RoomKind::Ongoing)); rooms.insert(room.kind(RoomKind::Ongoing));
} else { } else {
rooms.insert(room); rooms.insert(room);
@@ -349,23 +355,28 @@ impl Registry {
} }
pub(crate) fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) { pub(crate) fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
let mut room_map: HashMap<u64, usize> = HashMap::with_capacity(self.rooms.len()); let mut room_map: HashMap<u64, usize> = self
.rooms
for (index, room) in self.rooms.iter().enumerate() { .iter()
room_map.insert(room.read(cx).id, index); .enumerate()
} .map(|(idx, room)| (room.read(cx).id, idx))
.collect();
for new_room in rooms.into_iter() { for new_room in rooms.into_iter() {
// Check if we already have a room with this ID // Check if we already have a room with this ID
if let Some(&index) = room_map.get(&new_room.id) { if let Some(&index) = room_map.get(&new_room.id) {
self.rooms[index].update(cx, |this, cx| { self.rooms[index].update(cx, |this, cx| {
if new_room.created_at > this.created_at {
*this = new_room; *this = new_room;
cx.notify(); cx.notify();
}
}); });
} else { } else {
let new_index = self.rooms.len(); let new_room_id = new_room.id;
room_map.insert(new_room.id, new_index);
self.rooms.push(cx.new(|_| new_room)); 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) { 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 // Update room
room.update(cx, |this, cx| { room.update(cx, |this, cx| {
if is_new_event {
this.created_at(event.created_at, cx); this.created_at(event.created_at, cx);
}
// Set this room is ongoing if the new message is from current user // Set this room is ongoing if the new message is from current user
if author == identity { if author == identity {
@@ -433,8 +448,10 @@ impl Registry {
}); });
}); });
// Re-sort the rooms registry by their created at // Resort all rooms in the registry by their created at (after updated)
if is_new_event {
self.sort(cx); self.sort(cx);
}
} else { } else {
let room = Room::new(&event) let room = Room::new(&event)
.kind(RoomKind::default()) .kind(RoomKind::default())

View File

@@ -2,16 +2,37 @@ use std::hash::Hash;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Message { pub enum Message {
User(RenderedMessage), User(RenderedMessage),
System, System(Timestamp),
} }
impl Message { impl Message {
pub fn user(user: impl Into<RenderedMessage>) -> Self { pub fn user(user: impl Into<RenderedMessage>) -> Self {
Self::User(user.into()) 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<std::cmp::Ordering> {
Some(self.cmp(other))
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]