feat: bind user metadata to per-view directories (#64)

Reviewed-on: reya/coop#64
This commit is contained in:
2026-10-12 11:14:03 +00:00
parent 7c42466b0a
commit a365a8f584
21 changed files with 613 additions and 320 deletions
+10 -8
View File
@@ -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<InputState>,
/// Current user's contacts
contacts: Option<Vec<PublicKey>>,
/// Person directory for contacts and selected members
directory: Entity<Directory>,
/// Public keys selected as members of the chat
members: Vec<PublicKey>,
/// 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<Self>,
) -> Vec<AnyElement> {
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<Self>) -> 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()));
+14 -8
View File
@@ -9,7 +9,7 @@ use gpui::{
};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry, shorten_pubkey};
use person::{Directory, Person, PersonRegistry, shorten_pubkey};
use settings::AppSettings;
use state::{NostrRegistry, upload};
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
@@ -34,6 +34,8 @@ struct ProfileDialog {
public_key: PublicKey,
/// Whether the profile belongs to the signed-in user
is_current_user: bool,
/// Person directory for the profile
directory: Entity<Directory>,
/// Whether the metadata inputs are shown
editing: bool,
/// User's name text input
@@ -74,9 +76,15 @@ impl ProfileDialog {
.placeholder("A short introduce about you.")
});
let directory = Directory::new(cx);
directory.update(cx, |directory, cx| {
directory.track([public_key], cx);
});
Self {
public_key,
is_current_user,
directory,
editing: false,
name_input,
avatar_input,
@@ -90,8 +98,7 @@ impl ProfileDialog {
}
fn profile(&self, cx: &App) -> Person {
let persons = PersonRegistry::global(cx);
persons.read(cx).get(&self.public_key, cx)
self.directory.read(cx).person(&self.public_key, cx)
}
fn set_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -238,9 +245,8 @@ impl ProfileDialog {
}
fn save(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let persons = PersonRegistry::global(cx);
let public_key = self.public_key;
let old_metadata = persons.read(cx).get(&public_key, cx).metadata();
let old_metadata = self.directory.read(cx).person(&public_key, cx).metadata();
// Extract all new metadata fields
let avatar = self.avatar_input.read(cx).value();
@@ -274,9 +280,9 @@ impl ProfileDialog {
match task.await {
Ok(_) => {
this.update_in(cx, |this, window, cx| {
// Update the registry
persons.update(cx, |this, cx| {
this.insert(Person::new(public_key, new_metadata), cx);
// Publish the new metadata to every directory
PersonRegistry::global(cx).update(cx, |registry, cx| {
registry.insert(Person::new(public_key, new_metadata), cx);
});
this.set_updating(false, cx);
+11 -5
View File
@@ -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<Timestamp>,
/// All mutual contacts of the person being screened.
mutual_contacts: Vec<PublicKey>,
/// Person directory for the person and their mutual contacts
directory: Entity<Directory>,
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<Self>) -> 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<Self>) -> Option<Nip05Address> {
@@ -260,22 +265,23 @@ impl Screening {
fn mutual_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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()
+11 -3
View File
@@ -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<PublicKey>,
/// Person directory for the contacts
directory: Entity<Directory>,
/// 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<Self>) -> Vec<impl IntoElement> {
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()
+14 -3
View File
@@ -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<Group>, cx: &mut Context<Self>) {
self.group = Some(group.downgrade());
self.group_open = true;
let group = group.read(cx);
let mut keys: Vec<PublicKey> = 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<Self>,
) -> 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;
+1 -3
View File
@@ -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<Self>,
) -> Stateful<Div> {
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();
+3
View File
@@ -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();
})?;
+24 -8
View File
@@ -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<Vec<PublicKey>>,
/// Person directory for contacts, search hits, and group members
directory: Entity<Directory>,
/// Contacts selected in the contact picker.
selected_contacts: HashSet<PublicKey>,
/// Scroll state of the contact picker's list.
@@ -139,15 +142,27 @@ impl Sidebar {
);
subscriptions.push(
cx.subscribe_in(&nostr, window, |this, _, event, window, cx| {
if let StateEvent::NoSigner = event {
this.banner = onboarding::pick_banner();
if this.search_open {
this.toggle_search(window, cx);
cx.subscribe_in(
&nostr,
window,
|this, state, event, window, cx| match event {
StateEvent::NoSigner => {
this.banner = onboarding::pick_banner();
if this.search_open {
this.toggle_search(window, cx);
}
cx.notify();
}
cx.notify();
}
}),
StateEvent::SignerChanged => {
if let Some(current_user) = state.read(cx).current_user() {
this.directory.update(cx, |directory, cx| {
directory.track([current_user], cx);
});
}
}
_ => {}
},
),
);
subscriptions.push(cx.subscribe_in(
@@ -196,6 +211,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,
+3 -6
View File
@@ -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();
+14 -1
View File
@@ -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<PublicKey> = 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();