chore: fix rooms out of order while loading (#139)
* fix room out of order while loading * . * .
This commit is contained in:
28
Cargo.lock
generated
28
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -62,3 +62,4 @@ oneshot.workspace = true
|
||||
webbrowser.workspace = true
|
||||
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
indexset = "0.12.3"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -19,4 +19,3 @@ smol.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
fuzzy-matcher = "0.3.7"
|
||||
hashbrown = "0.15"
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user