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",
]
[[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",

View File

@@ -62,3 +62,4 @@ oneshot.workspace = true
webbrowser.workspace = true
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 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<Room>,
list_state: ListState,
messages: Vec<Message>,
rendered_texts_by_id: HashMap<EventId, RenderedText>,
reports_by_id: HashMap<EventId, Vec<SendReport>>,
messages: BTreeSet<Message>,
rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
reports_by_id: BTreeMap<EventId, Vec<SendReport>>,
// New Message
input: Entity<InputState>,
replies_to: Entity<Vec<EventId>>,
sending: bool,
// Media Attachment
attachments: Entity<Vec<Url>>,
@@ -75,7 +73,8 @@ pub struct Chat {
impl Chat {
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| {
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>) {
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<Self>) {
// 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<E>(&mut self, event: E, cx: &mut Context<Self>)
fn insert_message<E>(&mut self, event: E, _cx: &mut Context<Self>)
where
E: Into<RenderedMessage>,
{
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<E>(&mut self, events: E, cx: &mut Context<Self>)
where
E: IntoIterator,
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);
/// Convert and insert a vector of nostr events into the chat panel
fn insert_messages(&mut self, events: Vec<Event>, cx: &mut Context<Self>) {
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<Self>) -> AnyElement {
fn render_message_not_found(&self, ix: usize, cx: &Context<Self>) -> 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)
}
}),
)

View File

@@ -117,6 +117,17 @@ pub struct CoopSimpleStorage {
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 INGESTER: OnceLock<Ingester> = OnceLock::new();
static COOP_SIMPLE_STORAGE: OnceLock<CoopSimpleStorage> = 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 {

View File

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

View File

@@ -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>) {
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<Result<HashSet<Room>, 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<Room>, cx: &mut Context<Self>) {
let mut room_map: HashMap<u64, usize> = 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<u64, usize> = 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())

View File

@@ -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<RenderedMessage>) -> 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<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone)]