diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 77725881..1ccb2981 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -580,6 +580,7 @@ impl ChatRegistry { let room: Room = room.into().organize(&public_key); let room_id = room.id; let entity = cx.new(|_| room); + entity.update(cx, |room, cx| room.ensure_directory(cx)); self.room_index.insert(room_id, entity.clone()); self.rooms.insert(0, entity); @@ -594,6 +595,7 @@ impl ChatRegistry { if let hash_map::Entry::Vacant(e) = self.room_index.entry(id) { let entity = room.to_owned(); + entity.update(cx, |room, cx| room.ensure_directory(cx)); e.insert(entity.clone()); self.rooms.insert(0, entity); @@ -670,6 +672,7 @@ impl ChatRegistry { } else { let new_room_id = new_room.id; let entity = cx.new(|_| new_room); + entity.update(cx, |room, cx| room.ensure_directory(cx)); self.room_index.insert(new_room_id, entity.clone()); self.rooms.push(entity); diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 3b2cdb0c..7d48ac96 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -3,12 +3,12 @@ use std::hash::{Hash, Hasher}; use anyhow::{Error, anyhow}; use common::EventExt; -use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; +use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task}; use instant::Duration; use itertools::Itertools; use nip4e::DeviceRegistry; use nostr_sdk::prelude::*; -use person::{Person, PersonRegistry}; +use person::{Directory, Person, PersonRegistry}; use settings::{RoomConfig, SignerKind}; use state::{NostrRegistry, TIMEOUT, UniversalSigner}; @@ -88,6 +88,9 @@ pub struct Room { /// Configuration config: RoomConfig, + + /// Person directory, attached when the room is registered + directory: Option>, } impl Ord for Room { @@ -136,6 +139,7 @@ impl From<&UnsignedEvent> for Room { members, kind: RoomKind::default(), config: RoomConfig::new(), + directory: None, } } } @@ -236,6 +240,32 @@ impl Room { self.members.len() > 2 } + /// The room's person directory, attached when the room is registered + pub fn directory(&self) -> Option> { + self.directory.clone() + } + + /// Attach the person directory, tracking the room's members + pub(crate) fn ensure_directory(&mut self, cx: &mut Context) { + if self.directory.is_some() { + return; + } + + let directory = Directory::new(cx); + let members = self.members.clone(); + directory.update(cx, |directory, cx| directory.track(members, cx)); + self.directory = Some(directory); + } + + /// The person for a room member + fn member(&self, public_key: &PublicKey, cx: &App) -> Person { + let Some(directory) = self.directory.as_ref() else { + return Person::new(*public_key, Metadata::default()); + }; + + directory.read(cx).person(public_key, cx) + } + /// Gets the display name for the room pub fn display_name(&self, cx: &App) -> SharedString { if let Some(value) = self.subject.clone() { @@ -267,19 +297,16 @@ impl Room { /// /// Display member is always different from the current user. pub fn display_member(&self, cx: &App) -> Person { - let persons = PersonRegistry::global(cx); - persons.read(cx).get(&self.members[0], cx) + self.member(&self.members[0], cx) } /// Merge the names of the first two members of the room. fn merged_name(&self, cx: &App) -> SharedString { - let persons = PersonRegistry::global(cx); - if self.is_group() { let profiles: Vec = self .members .iter() - .map(|public_key| persons.read(cx).get(public_key, cx)) + .map(|public_key| self.member(public_key, cx)) .collect(); let mut name = profiles @@ -446,8 +473,6 @@ impl Room { // Build the `subject` + reply `e` tags + receiver `p` tags (excluding `sender`) fn conversation_tags(&self, replies: &[EventId], sender: PublicKey, cx: &App) -> Vec { - let persons = PersonRegistry::global(cx); - // Construct event's tags let mut tags = vec![]; @@ -463,7 +488,7 @@ impl Room { // Add all receiver tags (no intermediate allocation) for public_key in self.members.iter().filter(|pk| *pk != &sender) { - let member = persons.read(cx).get(public_key, cx); + let member = self.member(public_key, cx); tags.push(Tag::from(Nip01Tag::PublicKey { public_key: member.public_key(), relay_hint: member.messaging_relay_hint(), @@ -486,15 +511,14 @@ impl Room { let current_user = nostr.read(cx).current_user()?; // Get sender's profile - let persons = PersonRegistry::global(cx); - let sender = persons.read(cx).get(¤t_user, cx); + let sender = self.member(¤t_user, cx); // Get all members (excluding sender) let receivers: Vec = self .members .iter() .filter(|public_key| public_key != &&sender.public_key()) - .map(|member| persons.read(cx).get(member, cx)) + .map(|member| self.member(member, cx)) .collect(); Some(cx.background_spawn(async move { diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index c90a30e9..c5d71642 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -17,7 +17,7 @@ use gpui::{ }; use itertools::Itertools; use nostr_sdk::prelude::*; -use person::{Person, PersonRegistry}; +use person::{Directory, Person, PersonRegistry}; use regex::Regex; use settings::{AppSettings, SignerKind}; use smallvec::{SmallVec, smallvec}; @@ -73,6 +73,9 @@ pub struct ChatPanel { /// Chat Room room: WeakEntity, + /// Person directory shared with the room + directory: Entity, + /// Message list state list_state: ListState, @@ -133,6 +136,13 @@ impl ChatPanel { let messages = Vec::new(); let list_state = ListState::new(messages.len() + 1, ListAlignment::Bottom, px(1024.)); + // Share the room's person directory + let directory = room + .read_with(cx, |room, _cx| room.directory()) + .ok() + .flatten() + .unwrap_or_else(|| Directory::new(cx)); + // Get room id and name let (id, name) = room .read_with(cx, |this, _cx| { @@ -194,6 +204,7 @@ impl ChatPanel { message_index: HashMap::new(), reactions: BTreeMap::new(), room, + directory, list_state, input, subject_input, @@ -832,8 +843,7 @@ impl ChatPanel { } fn profile(&self, public_key: &PublicKey, cx: &App) -> Person { - let persons = PersonRegistry::global(cx); - persons.read(cx).get(public_key, cx) + self.directory.read(cx).person(public_key, cx) } fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context) { @@ -1085,7 +1095,7 @@ impl ChatPanel { } if let Some(message) = self.messages.get(index) { - let persons = PersonRegistry::global(cx); + let directory = self.directory.clone(); let show_author = self.is_group_start(index); let text = self .rendered_texts_by_id @@ -1095,7 +1105,7 @@ impl ChatPanel { &message.content, &message.mentions, &message.emojis, - &persons, + &directory, cx, ) }) @@ -1210,8 +1220,9 @@ impl ChatPanel { items } - fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement { + fn render_sent_reports(&self, id: &EventId, cx: &Context) -> impl IntoElement { let reports = self.sent_reports(id); + let panel = cx.weak_entity(); let pending = reports.as_ref().is_some_and(|reports| reports.is_empty()); @@ -1241,28 +1252,29 @@ impl ChatPanel { this.when(!pending, |this| { this.on_click(move |_e, window, cx| { let reports = reports.clone(); + let panel = panel.clone(); window.open_dialog(cx, move |this, _window, cx| { + let items = panel + .update(cx, |panel, cx| { + reports + .iter() + .map(|report| panel.render_report(report, cx)) + .collect::>() + }) + .unwrap_or_default(); + this.title(SharedString::from("Sent Reports")) .show_close(true) - .child(v_flex().gap_4().children({ - let mut items = Vec::with_capacity(reports.len()); - - for report in reports.iter() { - items.push(Self::render_report(report, cx)) - } - - items - })) + .child(v_flex().gap_4().children(items)) }); }) }) }) } - fn render_report(report: &SendReport, cx: &App) -> impl IntoElement { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&report.receiver, cx); + fn render_report(&self, report: &SendReport, cx: &App) -> AnyElement { + let profile = self.directory.read(cx).person(&report.receiver, cx); let name = profile.name(); let avatar = profile.avatar(); @@ -1363,6 +1375,7 @@ impl ChatPanel { }), ) }) + .into_any_element() } fn render_actions( @@ -1652,8 +1665,7 @@ impl ChatPanel { fn render_reply(&self, id: &EventId, cx: &Context) -> impl IntoElement { if let Some(text) = self.message(id) { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&text.author, cx); + let profile = self.directory.read(cx).person(&text.author, cx); div() .w_full() diff --git a/crates/chat_ui/src/text.rs b/crates/chat_ui/src/text.rs index 00ccf58a..8315fcef 100644 --- a/crates/chat_ui/src/text.rs +++ b/crates/chat_ui/src/text.rs @@ -1,7 +1,7 @@ use chat::{Mention, nevent_refs}; use gpui::{App, Entity}; use nostr_sdk::prelude::Url; -use person::PersonRegistry; +use person::Directory; use ui::markdown::InlineReplacement; pub use ui::markdown::RenderedText; @@ -9,7 +9,7 @@ pub fn rendered_text( content: &str, mentions: &[Mention], emojis: &[(String, Url)], - persons: &Entity, + persons: &Entity, cx: &App, ) -> RenderedText { let mut replacements = mentions @@ -17,7 +17,10 @@ pub fn rendered_text( .map(|mention| { InlineReplacement::new( mention.range.clone(), - format!("@{}", persons.read(cx).get(&mention.public_key, cx).name()), + format!( + "@{}", + persons.read(cx).person(&mention.public_key, cx).name() + ), ) }) .collect::>(); diff --git a/crates/nip29_ui/src/lib.rs b/crates/nip29_ui/src/lib.rs index e6eaddf1..ecb19bb3 100644 --- a/crates/nip29_ui/src/lib.rs +++ b/crates/nip29_ui/src/lib.rs @@ -11,7 +11,7 @@ use gpui::{ }; use nip29::{Group, GroupEvent, GroupKey, GroupsRegistry, Membership, Pin, Row}; use nostr_sdk::prelude::*; -use person::PersonRegistry; +use person::Directory; use serde::Deserialize; use settings::AppSettings; use smallvec::{SmallVec, smallvec}; @@ -308,6 +308,8 @@ pub struct GroupPanel { id: SharedString, focus_handle: FocusHandle, group: WeakEntity, + /// Person directory for members, message authors, and activity subjects + directory: Entity, list_state: ListState, head: Option, /// The message the composer is replying to, if any. @@ -356,12 +358,14 @@ impl GroupPanel { subscriptions.push(cx.subscribe(&group, |this, _group, event, cx| { if !matches!(event, GroupEvent::Sent(_)) { this.sync(cx); + this.track_participants(cx); cx.notify(); } })); subscriptions.push(cx.observe(&group, |this, _group, cx| { this.sync(cx); + this.track_participants(cx); cx.notify(); })); @@ -376,10 +380,11 @@ impl GroupPanel { )); }); - Self { + let mut this = Self { id, focus_handle: cx.focus_handle(), group: group.downgrade(), + directory: Directory::new(cx), list_state: ListState::new(count + 2, ListAlignment::Bottom, px(1024.)), head, reply_to: None, @@ -389,7 +394,29 @@ impl GroupPanel { uploading: false, tasks: smallvec![], _subscriptions: subscriptions, - } + }; + + this.track_participants(cx); + + this + } + + /// Track the keys rendered by this panel: members, message authors, and + /// activity subjects + fn track_participants(&mut self, cx: &mut Context) { + let Some(group) = self.group.upgrade() else { + return; + }; + + let group = group.read(cx); + let mut keys: Vec = group.members().iter().copied().collect(); + keys.extend(group.rows().iter().map(|row| match row { + Row::Message(message) => message.author, + Row::Activity(activity) => activity.subject(), + })); + + self.directory + .update(cx, |directory, cx| directory.track(keys, cx)); } fn item_count(&self, cx: &App) -> usize { @@ -877,11 +904,13 @@ impl GroupPanel { let message = match row { Row::Message(message) => message, - Row::Activity(activity) => return message::render_activity(ix, activity, cx), + Row::Activity(activity) => { + return message::render_activity(ix, activity, &self.directory, cx); + } }; let nostr = NostrRegistry::global(cx); - let persons = PersonRegistry::global(cx); + let directory = self.directory.clone(); let content = self .rendered_texts_by_id @@ -891,7 +920,7 @@ impl GroupPanel { &message.content, &message.mentions, &message.emojis, - &persons, + &directory, cx, ) }) @@ -916,6 +945,7 @@ impl GroupPanel { panel: cx.weak_entity(), admin, pinned, + persons: self.directory.clone(), }, cx, ) @@ -990,8 +1020,7 @@ impl GroupPanel { let context = self.group.upgrade().and_then(|entity| { entity.read_with(cx, |group, cx| { let message = group.message(target)?; - let persons = PersonRegistry::global(cx); - let author = persons.read(cx).get(&message.author, cx); + let author = self.directory.read(cx).person(&message.author, cx); let preview = message.preview(); let line = preview.lines().next().unwrap_or_default().trim(); diff --git a/crates/nip29_ui/src/message.rs b/crates/nip29_ui/src/message.rs index ea7edcf4..da5ac2e8 100644 --- a/crates/nip29_ui/src/message.rs +++ b/crates/nip29_ui/src/message.rs @@ -2,12 +2,12 @@ use chat::Message; use common::TimestampExt; use gpui::prelude::FluentBuilder; use gpui::{ - AnyElement, App, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, + AnyElement, App, Entity, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, WeakEntity, div, px, }; use nip29::{Activity, Group, Reaction}; use nostr_sdk::prelude::{EventId, Kind}; -use person::PersonRegistry; +use person::{Directory, PersonRegistry}; use settings::AppSettings; use state::NostrRegistry; use theme::{ActiveTheme, TextStyle, Typeset as _}; @@ -25,6 +25,7 @@ pub(crate) struct Actions { pub panel: WeakEntity, pub admin: bool, pub pinned: bool, + pub persons: Entity, } pub(crate) fn render( @@ -37,10 +38,10 @@ pub(crate) fn render( cx: &App, ) -> AnyElement { let nostr = NostrRegistry::global(cx); - let persons = PersonRegistry::global(cx); let hide_avatar = AppSettings::get_hide_avatar(cx); - let author = persons.read(cx).get(&message.author, cx); + let persons = actions.persons.clone(); + let author = persons.read(cx).person(&message.author, cx); let reaction_groups = group_reactions( reactions.iter().map(|reaction| { @@ -66,6 +67,7 @@ pub(crate) fn render( .when(!message.replies_to.is_empty(), |this| { this.children(replies( &message.replies_to, + &persons, &actions.group, &actions.panel, cx, @@ -185,9 +187,13 @@ fn react_menu( }) } -pub(crate) fn render_activity(ix: usize, activity: &Activity, cx: &App) -> AnyElement { - let persons = PersonRegistry::global(cx); - let subject = persons.read(cx).get(&activity.subject(), cx); +pub(crate) fn render_activity( + ix: usize, + activity: &Activity, + persons: &Entity, + cx: &App, +) -> AnyElement { + let subject = persons.read(cx).person(&activity.subject(), cx); let hide_avatar = AppSettings::get_hide_avatar(cx); h_flex() @@ -262,6 +268,7 @@ fn action(activity: &Activity) -> String { fn replies( targets: &[EventId], + persons: &Entity, group: &WeakEntity, panel: &WeakEntity, cx: &App, @@ -275,8 +282,7 @@ fn replies( for (ix, id) in targets.iter().enumerate() { let context = entity.read_with(cx, |group, cx| { let message = group.message(*id)?; - let persons = PersonRegistry::global(cx); - let author = persons.read(cx).get(&message.author, cx); + let author = persons.read(cx).person(&message.author, cx); Some((author.name(), message.preview())) }); diff --git a/crates/nip4e/src/lib.rs b/crates/nip4e/src/lib.rs index de761c21..698957b8 100644 --- a/crates/nip4e/src/lib.rs +++ b/crates/nip4e/src/lib.rs @@ -11,7 +11,7 @@ use gpui::{ SharedString, Styled, Subscription, Task, Window, div, px, relative, }; use nostr_sdk::prelude::*; -use person::PersonRegistry; +use person::Directory; use settings::AppSettings; use smallvec::{SmallVec, smallvec}; use state::{Announcement, CLIENT_NAME, NostrRegistry, UniversalSigner}; @@ -63,6 +63,8 @@ pub struct DeviceRegistry { pub(crate) pending_request: bool, /// Whether an announcement has been made for this device pub(crate) announcement_existed: Arc, + /// Person directory for requesters shown in notifications + directory: Entity, /// Relay notification listener notification_listener: Option>>, tasks: Vec>>, @@ -122,6 +124,7 @@ impl DeviceRegistry { signer, pending_request: false, announcement_existed: Arc::new(AtomicBool::new(false)), + directory: Directory::new(cx), notification_listener: None, tasks: vec![], _subscriptions: subscriptions, @@ -690,7 +693,11 @@ impl DeviceRegistry { } /// Build a notification for the encryption request. - fn notification(&self, event: Event, cx: &Context) -> Result { + fn notification( + &mut self, + event: Event, + cx: &mut Context, + ) -> Result { const MSG: &str = "You've requested an encryption key from another device. \ Approve to allow Coop to share with it. Before approving, \ compare the code with the one shown on the requesting \ @@ -705,8 +712,8 @@ impl DeviceRegistry { .context("Request has no valid requester key")?; let code = authorization_code(&requester); - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&requester, cx); + let directory = self.directory.clone(); + directory.update(cx, |directory, cx| directory.track([requester], cx)); let code_label = code.clone(); let entity = cx.entity().downgrade(); @@ -719,6 +726,8 @@ impl DeviceRegistry { .with_kind(NotificationKind::Info) .title("Encryption Key Request") .content(move |_this, _window, cx| { + let profile = directory.read(cx).person(&requester, cx); + v_flex() .gap(px(theme::SPACE)) .text_style(TextStyle::Callout) diff --git a/crates/workspace/src/dialogs/new_chat.rs b/crates/workspace/src/dialogs/new_chat.rs index 39796ffe..620cadca 100644 --- a/crates/workspace/src/dialogs/new_chat.rs +++ b/crates/workspace/src/dialogs/new_chat.rs @@ -10,7 +10,7 @@ use gpui::{ Window, div, px, uniform_list, }; use nostr_sdk::prelude::*; -use person::PersonRegistry; +use person::Directory; use settings::AppSettings; use state::NostrRegistry; use theme::{ActiveTheme, TextStyle, Typeset as _}; @@ -39,6 +39,8 @@ pub struct NewChat { input: Entity, /// Current user's contacts contacts: Option>, + /// Person directory for contacts and selected members + directory: Entity, /// Public keys selected as members of the chat members: Vec, /// Whether a NIP-05 address is being resolved @@ -69,6 +71,7 @@ impl NewChat { dock, input, contacts: None, + directory: Directory::new(cx), members: Vec::new(), resolving: false, error: None, @@ -112,6 +115,9 @@ impl NewChat { match task.await { Ok(contacts) => { this.update(cx, |this, cx| { + this.directory.update(cx, |directory, cx| { + directory.track(contacts.iter().copied(), cx) + }); this.contacts = Some(contacts); cx.notify(); })?; @@ -133,7 +139,6 @@ impl NewChat { return Vec::new(); }; - let persons = PersonRegistry::global(cx); let query = self.input.read(cx).value().trim().to_lowercase(); if query.is_empty() { @@ -146,7 +151,7 @@ impl NewChat { if public_key.to_hex().to_lowercase().contains(&query) { return true; } - let name = persons.read(cx).get(public_key, cx).name(); + let name = self.directory.read(cx).person(public_key, cx).name(); name.to_lowercase().contains(&query) }) .cloned() @@ -318,7 +323,6 @@ impl NewChat { contacts: &[PublicKey], cx: &mut Context, ) -> Vec { - let persons = PersonRegistry::global(cx); let start = range.start; contacts @@ -327,7 +331,7 @@ impl NewChat { .flatten() .enumerate() .map(|(offset, public_key)| { - let profile = persons.read(cx).get(public_key, cx); + let profile = self.directory.read(cx).person(public_key, cx); let public_key = *public_key; Nav::new(start + offset) @@ -350,13 +354,11 @@ impl NewChat { } fn render_members(&self, cx: &mut Context) -> impl IntoElement { - let persons = PersonRegistry::global(cx); - h_flex() .flex_wrap() .gap_1() .children(self.members.iter().map(|public_key| { - let profile = persons.read(cx).get(public_key, cx); + let profile = self.directory.read(cx).person(public_key, cx); let public_key = *public_key; let id = SharedString::from(format!("m-{}", public_key.to_hex())); diff --git a/crates/workspace/src/dialogs/screening.rs b/crates/workspace/src/dialogs/screening.rs index 18320bae..52b84221 100644 --- a/crates/workspace/src/dialogs/screening.rs +++ b/crates/workspace/src/dialogs/screening.rs @@ -9,7 +9,7 @@ use gpui::{ }; use instant::Duration; use nostr_sdk::prelude::*; -use person::{Person, PersonRegistry, shorten_pubkey}; +use person::{Directory, Person, shorten_pubkey}; use smallvec::{SmallVec, smallvec}; use state::{BOOTSTRAP_RELAYS, NostrAddress, NostrRegistry, TIMEOUT}; use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius}; @@ -33,6 +33,8 @@ pub struct Screening { last_active: Option, /// All mutual contacts of the person being screened. mutual_contacts: Vec, + /// Person directory for the person and their mutual contacts + directory: Entity, tasks: SmallVec<[Task<()>; 3]>, _subscriptions: SmallVec<[Subscription; 1]>, } @@ -53,12 +55,16 @@ impl Screening { this.verify_identifier(cx); }); + let directory = Directory::new(cx); + directory.update(cx, |directory, cx| directory.track([public_key], cx)); + Self { public_key, verified: false, followed: false, last_active: None, mutual_contacts: vec![], + directory, tasks: smallvec![], _subscriptions: subscriptions, } @@ -208,8 +214,7 @@ impl Screening { } fn profile(&self, cx: &Context) -> Person { - let persons = PersonRegistry::global(cx); - persons.read(cx).get(&self.public_key, cx) + self.directory.read(cx).person(&self.public_key, cx) } fn address(&self, cx: &Context) -> Option { @@ -260,22 +265,23 @@ impl Screening { fn mutual_contacts(&mut self, window: &mut Window, cx: &mut Context) { let contacts = self.mutual_contacts.clone(); + let directory = self.directory.clone(); window.open_dialog(cx, move |this, _window, _cx| { let contacts = contacts.clone(); + let directory = directory.clone(); let total = contacts.len(); this.title("Mutual contacts").child( v_flex().gap_1().pb_2().child( uniform_list("contacts", total, move |range, _window, cx| { - let persons = PersonRegistry::global(cx); let mut items = Vec::with_capacity(total); for ix in range { let Some(contact) = contacts.get(ix) else { continue; }; - let profile = persons.read(cx).get(contact, cx); + let profile = directory.read(cx).person(contact, cx); items.push( h_flex() diff --git a/crates/workspace/src/panels/contact_list.rs b/crates/workspace/src/panels/contact_list.rs index bf6633e0..085572eb 100644 --- a/crates/workspace/src/panels/contact_list.rs +++ b/crates/workspace/src/panels/contact_list.rs @@ -9,7 +9,7 @@ use gpui::{ }; use instant::Duration; use nostr_sdk::prelude::*; -use person::PersonRegistry; +use person::Directory; use smallvec::{SmallVec, smallvec}; use state::NostrRegistry; use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius, panel_radius}; @@ -41,6 +41,9 @@ pub struct ContactListPanel { /// All contacts contacts: HashSet, + /// Person directory for the contacts + directory: Entity, + /// Event subscriptions _subscriptions: SmallVec<[Subscription; 1]>, @@ -73,6 +76,7 @@ impl ContactListPanel { input, updating: false, contacts: HashSet::new(), + directory: Directory::new(cx), error: None, _subscriptions: subscriptions, tasks: vec![], @@ -110,6 +114,9 @@ impl ContactListPanel { // Update state this.update(cx, |this, cx| { + this.directory.update(cx, |directory, cx| { + directory.track(public_keys.iter().copied(), cx) + }); this.contacts.extend(public_keys); cx.notify(); })?; @@ -123,6 +130,8 @@ impl ContactListPanel { if let Ok(public_key) = PublicKey::parse(&value) { if self.contacts.insert(public_key) { + self.directory + .update(cx, |directory, cx| directory.track([public_key], cx)); self.input.update(cx, |this, cx| { this.set_value("", window, cx); }); @@ -218,11 +227,10 @@ impl ContactListPanel { } fn render_list_items(&mut self, cx: &mut Context) -> Vec { - let persons = PersonRegistry::global(cx); let mut items = Vec::new(); for (ix, public_key) in self.contacts.iter().enumerate() { - let profile = persons.read(cx).get(public_key, cx); + let profile = self.directory.read(cx).person(public_key, cx); items.push( h_flex() diff --git a/crates/workspace/src/sidebar/group_view.rs b/crates/workspace/src/sidebar/group_view.rs index 01ae3f2e..687109f1 100644 --- a/crates/workspace/src/sidebar/group_view.rs +++ b/crates/workspace/src/sidebar/group_view.rs @@ -8,7 +8,6 @@ use gpui::{ }; use nip29::Group; use nostr_sdk::prelude::*; -use person::PersonRegistry; use settings::AppSettings; use state::NostrRegistry; use theme::{ActiveTheme, TextStyle, Typeset as _}; @@ -51,6 +50,19 @@ impl Sidebar { pub(super) fn show_group(&mut self, group: &Entity, cx: &mut Context) { self.group = Some(group.downgrade()); self.group_open = true; + + let group = group.read(cx); + let mut keys: Vec = group.members().iter().copied().collect(); + keys.extend( + group + .admins() + .entries() + .iter() + .map(|(public_key, _)| *public_key), + ); + self.directory + .update(cx, |directory, cx| directory.track(keys, cx)); + cx.notify(); } @@ -253,9 +265,8 @@ impl Sidebar { cx: &mut Context, ) -> AnyElement { let hide_avatar = AppSettings::get_hide_avatar(cx); - let persons = PersonRegistry::global(cx); - let person = persons.read(cx).get(public_key, cx); + let person = self.directory.read(cx).person(public_key, cx); let group = group.clone(); let public_key = *public_key; diff --git a/crates/workspace/src/sidebar/header.rs b/crates/workspace/src/sidebar/header.rs index 35309497..f0e6e469 100644 --- a/crates/workspace/src/sidebar/header.rs +++ b/crates/workspace/src/sidebar/header.rs @@ -5,7 +5,6 @@ use gpui::{ Stateful, Styled, div, px, }; use nostr_sdk::prelude::*; -use person::PersonRegistry; use theme::TABBAR_HEIGHT; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; @@ -22,8 +21,7 @@ impl Sidebar { cx: &mut Context, ) -> Stateful
{ let updater = AutoUpdater::try_global(cx); - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(current_user, cx); + let profile = self.directory.read(cx).person(current_user, cx); let avatar = profile.avatar(); let avatar_seed = profile.avatar_seed(); diff --git a/crates/workspace/src/sidebar/inbox.rs b/crates/workspace/src/sidebar/inbox.rs index 8e0bb126..0013bbdc 100644 --- a/crates/workspace/src/sidebar/inbox.rs +++ b/crates/workspace/src/sidebar/inbox.rs @@ -69,6 +69,9 @@ impl Sidebar { match task.await { Ok(contacts) => { this.update(cx, |this, cx| { + this.directory.update(cx, |directory, cx| { + directory.track(contacts.iter().copied(), cx) + }); this.contacts = Some(contacts); cx.notify(); })?; diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 45d7d471..00ec1093 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -13,6 +13,7 @@ use gpui::{ use instant::Duration; use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry}; use nostr_sdk::prelude::*; +use person::Directory; use smallvec::{SmallVec, smallvec}; use state::{FIND_DELAY, NostrRegistry, StateEvent}; use theme::{ActiveTheme, TABBAR_HEIGHT, TextStyle, Typeset as _}; @@ -61,6 +62,8 @@ pub struct Sidebar { contacts_open: bool, /// The signed-in user's contacts, loaded when the contact picker opens. contacts: Option>, + /// Person directory for contacts, search hits, and group members + directory: Entity, /// Contacts selected in the contact picker. selected_contacts: HashSet, /// Scroll state of the contact picker's list. @@ -196,6 +199,7 @@ impl Sidebar { chats_open: None, contacts_open: false, contacts: None, + directory: Directory::new(cx), selected_contacts: HashSet::new(), contacts_scroll: UniformListScrollHandle::new(), requests_open: false, diff --git a/crates/workspace/src/sidebar/rows.rs b/crates/workspace/src/sidebar/rows.rs index 3c46914b..0ed656a7 100644 --- a/crates/workspace/src/sidebar/rows.rs +++ b/crates/workspace/src/sidebar/rows.rs @@ -9,7 +9,6 @@ use gpui::{ }; use nip29::Group; use nostr_sdk::prelude::*; -use person::PersonRegistry; use settings::AppSettings; use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius}; use ui::avatar::Avatar; @@ -196,8 +195,7 @@ impl Sidebar { .into_any_element() } SidebarRow::Contact { public_key } => { - let persons = PersonRegistry::global(cx); - let person = persons.read(cx).get(public_key, cx); + let person = self.directory.read(cx).person(public_key, cx); let selected = self.selected_contacts.contains(public_key); let sidebar = cx.entity().downgrade(); let public_key = public_key.to_owned(); @@ -224,8 +222,7 @@ impl Sidebar { .into_any_element() } SidebarRow::Profile { public_key } => { - let persons = PersonRegistry::global(cx); - let person = persons.read(cx).get(public_key, cx); + let person = self.directory.read(cx).person(public_key, cx); let public_key = public_key.to_owned(); Nav::new(index) @@ -330,7 +327,7 @@ impl Sidebar { .into_any_element() } SidebarRow::Message { hit } => { - let person = PersonRegistry::global(cx).read(cx).get(&hit.author, cx); + let person = self.directory.read(cx).person(&hit.author, cx); let name = hit.room.read(cx).display_name(cx); let detail = format!("{} ยท {}", name, hit.created_at.to_ago()); let dock = self.dock.clone(); diff --git a/crates/workspace/src/sidebar/search.rs b/crates/workspace/src/sidebar/search.rs index 715487eb..39e9d429 100644 --- a/crates/workspace/src/sidebar/search.rs +++ b/crates/workspace/src/sidebar/search.rs @@ -106,6 +106,14 @@ impl Sidebar { .chain(groups.into_iter().map(SearchResult::Group)) .chain(messages.into_iter().map(SearchResult::Message)) .collect(); + + let authors = self.results.iter().filter_map(|result| match result { + SearchResult::Message(hit) => Some(hit.author), + _ => None, + }); + self.directory + .update(cx, |directory, cx| directory.track(authors, cx)); + cx.notify(); } @@ -133,7 +141,7 @@ impl Sidebar { this.update(cx, |this, cx| { this.global_finding = false; - this.global_results = Some(match result { + let keys: Vec = match result { Ok(profiles) => { // Relays may return the same profile more than once let mut seen = HashSet::new(); @@ -146,7 +154,12 @@ impl Sidebar { log::debug!("Global search failed: {error}"); Vec::new() } + }; + + this.directory.update(cx, |directory, cx| { + directory.track(keys.iter().copied(), cx) }); + this.global_results = Some(keys); cx.notify(); }) .ok();