chore: improve performance (#42)
* use uniform list for rooms list * move profile cache to outside gpui context * update comment * refactor * refactor * . * . * add avatar component * . * refactor * .
This commit is contained in:
@@ -8,7 +8,6 @@ publish.workspace = true
|
||||
account = { path = "../account" }
|
||||
common = { path = "../common" }
|
||||
global = { path = "../global" }
|
||||
ui = { path = "../ui" }
|
||||
|
||||
gpui.workspace = true
|
||||
nostr.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::{BTreeMap, HashMap, HashSet}
|
||||
collections::{HashMap, HashSet},
|
||||
};
|
||||
|
||||
use account::Account;
|
||||
@@ -8,19 +8,21 @@ use anyhow::Error;
|
||||
use common::room_hash;
|
||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
use global::get_client;
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use room::RoomKind;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use ui::ContextModal;
|
||||
|
||||
use crate::room::Room;
|
||||
|
||||
mod constants;
|
||||
pub mod message;
|
||||
pub mod room;
|
||||
|
||||
mod constants;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
ChatRegistry::set_global(cx.new(ChatRegistry::new), cx);
|
||||
}
|
||||
@@ -29,6 +31,9 @@ struct GlobalChatRegistry(Entity<ChatRegistry>);
|
||||
|
||||
impl Global for GlobalChatRegistry {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NewRoom(pub WeakEntity<Room>);
|
||||
|
||||
/// Main registry for managing chat rooms and user profiles
|
||||
///
|
||||
/// The ChatRegistry is responsible for:
|
||||
@@ -37,8 +42,6 @@ impl Global for GlobalChatRegistry {}
|
||||
/// - Loading room data from the lmdb
|
||||
/// - Handling messages and room creation
|
||||
pub struct ChatRegistry {
|
||||
/// Map of user public keys to their profile metadata
|
||||
profiles: Entity<BTreeMap<PublicKey, Option<Metadata>>>,
|
||||
/// Collection of all chat rooms
|
||||
pub rooms: Vec<Entity<Room>>,
|
||||
/// Indicates if rooms are currently being loaded
|
||||
@@ -50,6 +53,8 @@ pub struct ChatRegistry {
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl EventEmitter<NewRoom> for ChatRegistry {}
|
||||
|
||||
impl ChatRegistry {
|
||||
/// Retrieve the Global ChatRegistry instance
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
@@ -68,7 +73,6 @@ impl ChatRegistry {
|
||||
|
||||
/// Create a new ChatRegistry instance
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
let profiles = cx.new(|_| BTreeMap::new());
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
// When the ChatRegistry is created, load all rooms from the local database
|
||||
@@ -79,30 +83,13 @@ impl ChatRegistry {
|
||||
}));
|
||||
|
||||
// When any Room is created, load metadata for all members
|
||||
subscriptions.push(cx.observe_new::<Room>(|this, window, cx| {
|
||||
if let Some(window) = window {
|
||||
let task = this.load_metadata(cx);
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if let Ok(data) = task.await {
|
||||
cx.update(|_, cx| {
|
||||
for (public_key, metadata) in data.into_iter() {
|
||||
Self::global(cx).update(cx, |this, cx| {
|
||||
this.add_profile(public_key, metadata, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
subscriptions.push(cx.observe_new::<Room>(|this, _window, cx| {
|
||||
this.load_metadata(cx).detach();
|
||||
}));
|
||||
|
||||
Self {
|
||||
rooms: vec![],
|
||||
wait_for_eose: true,
|
||||
profiles,
|
||||
subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -115,11 +102,31 @@ impl ChatRegistry {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Get rooms by its kind.
|
||||
pub fn rooms_by_kind(&self, kind: RoomKind, cx: &App) -> Vec<Entity<Room>> {
|
||||
/// Get room by its position.
|
||||
pub fn room_by_ix(&self, ix: usize, _cx: &App) -> Option<&Entity<Room>> {
|
||||
self.rooms.get(ix)
|
||||
}
|
||||
|
||||
/// Get all ongoing rooms.
|
||||
pub fn ongoing_rooms(&self, cx: &App) -> Vec<Entity<Room>> {
|
||||
self.rooms
|
||||
.iter()
|
||||
.filter(|room| room.read(cx).kind == kind)
|
||||
.filter(|room| room.read(cx).kind == RoomKind::Ongoing)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all request rooms.
|
||||
pub fn request_rooms(&self, trusted_only: bool, cx: &App) -> Vec<Entity<Room>> {
|
||||
self.rooms
|
||||
.iter()
|
||||
.filter(|room| {
|
||||
if trusted_only {
|
||||
room.read(cx).kind == RoomKind::Trusted
|
||||
} else {
|
||||
room.read(cx).kind != RoomKind::Ongoing
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
@@ -189,10 +196,9 @@ impl ChatRegistry {
|
||||
.filter(|ev| ev.tags.public_keys().peekable().peek().is_some())
|
||||
{
|
||||
let hash = room_hash(&event);
|
||||
|
||||
let mut is_trust = trusted_keys.contains(&event.pubkey);
|
||||
|
||||
if is_trust == false {
|
||||
if !is_trust {
|
||||
// Check if room's author is seen in any contact list
|
||||
let filter = Filter::new().kind(Kind::ContactList).pubkey(event.pubkey);
|
||||
// If room's author is seen at least once, mark as trusted
|
||||
@@ -256,71 +262,25 @@ impl ChatRegistry {
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Add a user profile to the registry
|
||||
///
|
||||
/// Only adds the profile if it doesn't already exist or is currently none
|
||||
pub fn add_profile(
|
||||
&mut self,
|
||||
public_key: PublicKey,
|
||||
metadata: Option<Metadata>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.profiles.update(cx, |this, _cx| {
|
||||
this.entry(public_key)
|
||||
.and_modify(|entry| {
|
||||
if entry.is_none() {
|
||||
*entry = metadata.clone();
|
||||
}
|
||||
})
|
||||
.or_insert_with(|| metadata);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get a user profile by public key
|
||||
pub fn profile(&self, public_key: &PublicKey, cx: &App) -> Profile {
|
||||
let metadata = if let Some(profile) = self.profiles.read(cx).get(public_key) {
|
||||
profile.clone().unwrap_or_default()
|
||||
/// Push a new Room to the global registry
|
||||
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
|
||||
let weak_room = if let Some(room) = self
|
||||
.rooms
|
||||
.iter()
|
||||
.find(|this| this.read(cx).id == room.read(cx).id)
|
||||
{
|
||||
room.downgrade()
|
||||
} else {
|
||||
Metadata::default()
|
||||
};
|
||||
let weak_room = room.downgrade();
|
||||
|
||||
Profile::new(*public_key, metadata)
|
||||
}
|
||||
|
||||
/// Push a Room Entity to the global registry
|
||||
///
|
||||
/// Returns the ID of the room
|
||||
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) -> u64 {
|
||||
let id = room.read(cx).id;
|
||||
|
||||
if !self.rooms.iter().any(|this| this.read(cx) == room.read(cx)) {
|
||||
// Add this room to the global registry
|
||||
self.rooms.insert(0, room);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
weak_room
|
||||
};
|
||||
|
||||
/// Parse a Nostr event into a Coop Room and push it to the global registry
|
||||
///
|
||||
/// Returns the ID of the new room
|
||||
pub fn event_to_room(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> u64 {
|
||||
let room = Room::new(event).kind(RoomKind::Ongoing);
|
||||
let id = room.id;
|
||||
|
||||
if !self.rooms.iter().any(|this| this.read(cx) == &room) {
|
||||
self.rooms.insert(0, cx.new(|_| room));
|
||||
cx.notify();
|
||||
} else {
|
||||
window.push_notification("Room already exists", cx);
|
||||
}
|
||||
|
||||
id
|
||||
cx.emit(NewRoom(weak_room));
|
||||
}
|
||||
|
||||
/// Parse a Nostr event into a Coop Message and push it to the belonging room
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::{cmp::Ordering, sync::Arc};
|
||||
use account::Account;
|
||||
use anyhow::{anyhow, Error};
|
||||
use chrono::{Local, TimeZone};
|
||||
use common::{compare, profile::SharedProfile, room_hash};
|
||||
use global::get_client;
|
||||
use common::{compare, profile::RenderProfile, room_hash};
|
||||
use global::{async_cache_profile, get_cache_profile, get_client, profiles};
|
||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -12,7 +12,6 @@ use nostr_sdk::prelude::*;
|
||||
use crate::{
|
||||
constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE},
|
||||
message::Message,
|
||||
ChatRegistry,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -157,20 +156,6 @@ impl Room {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Gets the profile for a specific public key
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `public_key` - The public key to get the profile for
|
||||
/// * `cx` - The App context
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The Profile associated with the given public key
|
||||
pub fn profile_by_pubkey(&self, public_key: &PublicKey, cx: &App) -> Profile {
|
||||
ChatRegistry::global(cx).read(cx).profile(public_key, cx)
|
||||
}
|
||||
|
||||
/// Gets the first member in the room that isn't the current user
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -183,7 +168,7 @@ impl Room {
|
||||
pub fn first_member(&self, cx: &App) -> Profile {
|
||||
let account = Account::global(cx).read(cx);
|
||||
let Some(profile) = account.profile.clone() else {
|
||||
return self.profile_by_pubkey(&self.members[0], cx);
|
||||
return get_cache_profile(&self.members[0]);
|
||||
};
|
||||
|
||||
if let Some(public_key) = self
|
||||
@@ -193,7 +178,7 @@ impl Room {
|
||||
.collect::<Vec<_>>()
|
||||
.first()
|
||||
{
|
||||
self.profile_by_pubkey(public_key, cx)
|
||||
get_cache_profile(public_key)
|
||||
} else {
|
||||
profile
|
||||
}
|
||||
@@ -215,13 +200,13 @@ impl Room {
|
||||
let profiles = self
|
||||
.members
|
||||
.iter()
|
||||
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
|
||||
.map(get_cache_profile)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut name = profiles
|
||||
.iter()
|
||||
.take(2)
|
||||
.map(|profile| profile.shared_name())
|
||||
.map(|profile| profile.render_name())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
@@ -231,7 +216,7 @@ impl Room {
|
||||
|
||||
name.into()
|
||||
} else {
|
||||
self.first_member(cx).shared_name()
|
||||
self.first_member(cx).render_name()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +254,7 @@ impl Room {
|
||||
if let Some(picture) = self.picture.as_ref() {
|
||||
picture.clone()
|
||||
} else if !self.is_group() {
|
||||
self.first_member(cx).shared_avatar()
|
||||
self.first_member(cx).render_avatar()
|
||||
} else {
|
||||
"brand/group.png".into()
|
||||
}
|
||||
@@ -327,22 +312,27 @@ impl Room {
|
||||
///
|
||||
/// A Task that resolves to Result<Vec<(PublicKey, Option<Metadata>)>, Error>
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn load_metadata(
|
||||
&self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<(PublicKey, Option<Metadata>)>, Error>> {
|
||||
pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
|
||||
let client = get_client();
|
||||
let public_keys = Arc::clone(&self.members);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut output = vec![];
|
||||
|
||||
for public_key in public_keys.iter() {
|
||||
let metadata = client.database().metadata(*public_key).await?;
|
||||
output.push((*public_key, metadata));
|
||||
|
||||
profiles()
|
||||
.write()
|
||||
.await
|
||||
.entry(*public_key)
|
||||
.and_modify(|entry| {
|
||||
if entry.is_none() {
|
||||
*entry = metadata.clone();
|
||||
}
|
||||
})
|
||||
.or_insert_with(|| metadata);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -391,10 +381,6 @@ impl Room {
|
||||
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Message>, Error>> {
|
||||
let client = get_client();
|
||||
let pubkeys = Arc::clone(&self.members);
|
||||
let profiles: Vec<Profile> = pubkeys
|
||||
.iter()
|
||||
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
|
||||
.collect();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::PrivateDirectMessage)
|
||||
@@ -442,12 +428,6 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
let author = profiles
|
||||
.iter()
|
||||
.find(|profile| profile.public_key() == event.pubkey)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Profile::new(event.pubkey, Metadata::default()));
|
||||
|
||||
let pubkey_tokens = tokens
|
||||
.filter_map(|token| match token {
|
||||
Token::Nostr(nip21) => match nip21 {
|
||||
@@ -459,16 +439,12 @@ impl Room {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for pubkey in pubkey_tokens {
|
||||
mentions.push(
|
||||
profiles
|
||||
.iter()
|
||||
.find(|profile| profile.public_key() == pubkey)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Profile::new(pubkey, Metadata::default())),
|
||||
);
|
||||
for pubkey in pubkey_tokens.iter() {
|
||||
mentions.push(async_cache_profile(pubkey).await);
|
||||
}
|
||||
|
||||
let author = async_cache_profile(&event.pubkey).await;
|
||||
|
||||
if let Ok(message) = Message::builder()
|
||||
.id(event.id)
|
||||
.content(content)
|
||||
@@ -498,10 +474,10 @@ impl Room {
|
||||
///
|
||||
/// Processes the event and emits an Incoming to the UI when complete
|
||||
pub fn emit_message(&self, event: Event, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let author = ChatRegistry::get_global(cx).profile(&event.pubkey, cx);
|
||||
let author = get_cache_profile(&event.pubkey);
|
||||
|
||||
// Extract all mentions from content
|
||||
let mentions = extract_mentions(&event.content, cx);
|
||||
let mentions = extract_mentions(&event.content);
|
||||
|
||||
// Extract reply_to if present
|
||||
let mut replies_to = vec![];
|
||||
@@ -583,7 +559,7 @@ impl Room {
|
||||
event.ensure_id();
|
||||
|
||||
// Extract all mentions from content
|
||||
let mentions = extract_mentions(&event.content, cx);
|
||||
let mentions = extract_mentions(&event.content);
|
||||
|
||||
// Extract reply_to if present
|
||||
let mut replies_to = vec![];
|
||||
@@ -727,13 +703,11 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_mentions(content: &str, cx: &App) -> Vec<Profile> {
|
||||
pub fn extract_mentions(content: &str) -> Vec<Profile> {
|
||||
let parser = NostrParser::new();
|
||||
let tokens = parser.parse(content);
|
||||
let mut mentions = vec![];
|
||||
|
||||
let profiles = ChatRegistry::get_global(cx).profiles.read(cx);
|
||||
|
||||
let pubkey_tokens = tokens
|
||||
.filter_map(|token| match token {
|
||||
Token::Nostr(nip21) => match nip21 {
|
||||
@@ -746,9 +720,7 @@ pub fn extract_mentions(content: &str, cx: &App) -> Vec<Profile> {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for pubkey in pubkey_tokens.into_iter() {
|
||||
if let Some(metadata) = profiles.get(&pubkey).cloned() {
|
||||
mentions.push(Profile::new(pubkey, metadata.unwrap_or_default()));
|
||||
}
|
||||
mentions.push(get_cache_profile(&pubkey));
|
||||
}
|
||||
|
||||
mentions
|
||||
|
||||
Reference in New Issue
Block a user