This commit is contained in:
2026-01-07 17:22:59 +07:00
parent 967355cd4a
commit bf586580ab
14 changed files with 214 additions and 81 deletions

View File

@@ -4,11 +4,11 @@ use std::hash::{Hash, Hasher};
use std::time::Duration; use std::time::Duration;
use anyhow::Error; use anyhow::Error;
use common::{EventUtils, RenderedProfile}; use common::EventUtils;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::{Person, PersonRegistry};
use state::{tracker, NostrRegistry}; use state::{tracker, NostrRegistry};
use crate::NewMessage; use crate::NewMessage;
@@ -264,9 +264,9 @@ impl Room {
} }
/// Gets the display image for the room /// Gets the display image for the room
pub fn display_image(&self, proxy: bool, cx: &App) -> SharedString { pub fn display_image(&self, cx: &App) -> SharedString {
if !self.is_group() { if !self.is_group() {
self.display_member(cx).avatar(proxy) self.display_member(cx).avatar()
} else { } else {
SharedString::from("brand/group.png") SharedString::from("brand/group.png")
} }
@@ -275,7 +275,7 @@ impl Room {
/// Get a member to represent the room /// Get a member to represent the room
/// ///
/// Display member is always different from the current user. /// Display member is always different from the current user.
pub fn display_member(&self, cx: &App) -> Profile { pub fn display_member(&self, cx: &App) -> Person {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key(); let public_key = nostr.read(cx).identity().read(cx).public_key();
@@ -295,7 +295,7 @@ impl Room {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
if self.is_group() { if self.is_group() {
let profiles: Vec<Profile> = self let profiles: Vec<Person> = self
.members .members
.iter() .iter()
.map(|public_key| persons.read(cx).get(public_key, cx)) .map(|public_key| persons.read(cx).get(public_key, cx))
@@ -314,7 +314,7 @@ impl Room {
SharedString::from(name) SharedString::from(name)
} else { } else {
self.display_member(cx).display_name() self.display_member(cx).name()
} }
} }

View File

@@ -3,7 +3,7 @@ use std::time::Duration;
pub use actions::*; pub use actions::*;
use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport}; use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport};
use common::{nip96_upload, RenderedProfile, RenderedTimestamp}; use common::{nip96_upload, RenderedTimestamp};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext, div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext,
@@ -16,7 +16,7 @@ use gpui_tokio::Tokio;
use indexset::{BTreeMap, BTreeSet}; use indexset::{BTreeMap, BTreeSet};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::{Person, PersonRegistry};
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs; use smol::fs;
@@ -506,7 +506,7 @@ impl ChatPanel {
}); });
} }
fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Profile { fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Person {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
persons.read(cx).get(public_key, cx) persons.read(cx).get(public_key, cx)
} }
@@ -632,7 +632,7 @@ impl ChatPanel {
this.child( this.child(
div() div()
.id(SharedString::from(format!("{ix}-avatar"))) .id(SharedString::from(format!("{ix}-avatar")))
.child(Avatar::new(author.avatar(proxy)).size(rems(2.))) .child(Avatar::new(author.avatar()).size(rems(2.)))
.context_menu(move |this, _window, _cx| { .context_menu(move |this, _window, _cx| {
let view = Box::new(OpenPublicKey(public_key)); let view = Box::new(OpenPublicKey(public_key));
let copy = Box::new(CopyPublicKey(public_key)); let copy = Box::new(CopyPublicKey(public_key));
@@ -657,7 +657,7 @@ impl ChatPanel {
div() div()
.font_semibold() .font_semibold()
.text_color(cx.theme().text) .text_color(cx.theme().text)
.child(author.display_name()), .child(author.name()),
) )
.child(message.created_at.to_human_time()) .child(message.created_at.to_human_time())
.when_some(is_sent_success, |this, status| { .when_some(is_sent_success, |this, status| {
@@ -714,7 +714,7 @@ impl ChatPanel {
.child( .child(
div() div()
.text_color(cx.theme().text_accent) .text_color(cx.theme().text_accent)
.child(author.display_name()), .child(author.name()),
) )
.child( .child(
div() div()
@@ -796,8 +796,8 @@ impl ChatPanel {
fn render_report(report: &SendReport, cx: &App) -> impl IntoElement { fn render_report(report: &SendReport, cx: &App) -> impl IntoElement {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&report.receiver, cx); let profile = persons.read(cx).get(&report.receiver, cx);
let name = profile.display_name(); let name = profile.name();
let avatar = profile.avatar(true); let avatar = profile.avatar();
v_flex() v_flex()
.gap_2() .gap_2()
@@ -1080,7 +1080,7 @@ impl ChatPanel {
.child( .child(
div() div()
.text_color(cx.theme().text_accent) .text_color(cx.theme().text_accent)
.child(profile.display_name()), .child(profile.name()),
), ),
) )
.child( .child(
@@ -1134,7 +1134,7 @@ impl Panel for ChatPanel {
.read_with(cx, |this, cx| { .read_with(cx, |this, cx| {
let proxy = AppSettings::get_proxy_user_avatars(cx); let proxy = AppSettings::get_proxy_user_avatars(cx);
let label = this.display_name(cx); let label = this.display_name(cx);
let url = this.display_image(proxy, cx); let url = this.display_image(cx);
h_flex() h_flex()
.gap_1p5() .gap_1p5()

View File

@@ -1,7 +1,6 @@
use std::ops::Range; use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
use common::RenderedProfile;
use gpui::{ use gpui::{
AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString, AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString,
StyledText, UnderlineStyle, Window, StyledText, UnderlineStyle, Window,
@@ -255,7 +254,7 @@ fn render_pubkey(
) { ) {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx); let profile = persons.read(cx).get(&public_key, cx);
let display_name = format!("@{}", profile.display_name()); let display_name = format!("@{}", profile.name());
text.replace_range(range.clone(), &display_name); text.replace_range(range.clone(), &display_name);

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use auto_update::{AutoUpdateStatus, AutoUpdater}; use auto_update::{AutoUpdateStatus, AutoUpdater};
use chat::{ChatEvent, ChatRegistry}; use chat::{ChatEvent, ChatRegistry};
use chat_ui::{CopyPublicKey, OpenPublicKey}; use chat_ui::{CopyPublicKey, OpenPublicKey};
use common::{RenderedProfile, DEFAULT_SIDEBAR_WIDTH}; use common::DEFAULT_SIDEBAR_WIDTH;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
deferred, div, px, relative, rems, App, AppContext, Axis, ClipboardItem, Context, Entity, deferred, div, px, relative, rems, App, AppContext, Axis, ClipboardItem, Context, Entity,
@@ -14,7 +14,6 @@ use key_store::{Credential, KeyItem, KeyStore};
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use relay_auth::RelayAuth; use relay_auth::RelayAuth;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry}; use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry};
@@ -257,9 +256,9 @@ impl ChatSpace {
this.update_in(cx, |_, window, cx| { this.update_in(cx, |_, window, cx| {
match result { match result {
Ok(profile) => { Ok(person) => {
persons.update(cx, |this, cx| { persons.update(cx, |this, cx| {
this.insert(profile, cx); this.insert(person, cx);
// Close the edit profile modal // Close the edit profile modal
window.close_all_modals(cx); window.close_all_modals(cx);
}); });
@@ -476,7 +475,6 @@ impl ChatSpace {
} }
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let auto_update = AutoUpdater::global(cx); let auto_update = AutoUpdater::global(cx);
let relay_auth = RelayAuth::global(cx); let relay_auth = RelayAuth::global(cx);
@@ -562,9 +560,9 @@ impl ChatSpace {
.reverse() .reverse()
.transparent() .transparent()
.icon(IconName::CaretDown) .icon(IconName::CaretDown)
.child(Avatar::new(profile.avatar(proxy)).size(rems(1.45))) .child(Avatar::new(profile.avatar()).size(rems(1.45)))
.popup_menu(move |this, _window, _cx| { .popup_menu(move |this, _window, _cx| {
this.label(profile.display_name()) this.label(profile.name())
.menu_with_icon( .menu_with_icon(
"Profile", "Profile",
IconName::EmojiFill, IconName::EmojiFill,

View File

@@ -13,7 +13,6 @@ use gpui::{
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use list_item::RoomListItem; use list_item::RoomListItem;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION}; use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION};
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -540,7 +539,6 @@ impl Sidebar {
range: Range<usize>, range: Range<usize>,
cx: &Context<Self>, cx: &Context<Self>,
) -> Vec<impl IntoElement> { ) -> Vec<impl IntoElement> {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let mut items = Vec::with_capacity(range.end - range.start); let mut items = Vec::with_capacity(range.end - range.start);
for ix in range { for ix in range {
@@ -563,7 +561,7 @@ impl Sidebar {
RoomListItem::new(ix) RoomListItem::new(ix)
.room_id(room_id) .room_id(room_id)
.name(this.display_name(cx)) .name(this.display_name(cx))
.avatar(this.display_image(proxy, cx)) .avatar(this.display_image(cx))
.public_key(member.public_key()) .public_key(member.public_key())
.kind(this.kind) .kind(this.kind)
.created_at(this.created_at.to_ago()) .created_at(this.created_at.to_ago())

View File

@@ -10,6 +10,7 @@ use gpui::{
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::Person;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs; use smol::fs;
@@ -233,7 +234,7 @@ impl UserProfile {
.detach(); .detach();
} }
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Profile, Error>> { pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Person, Error>> {
let avatar = self.avatar_input.read(cx).value().to_string(); let avatar = self.avatar_input.read(cx).value().to_string();
let name = self.name_input.read(cx).value().to_string(); let name = self.name_input.read(cx).value().to_string();
let bio = self.bio_input.read(cx).value().to_string(); let bio = self.bio_input.read(cx).value().to_string();
@@ -274,7 +275,7 @@ impl UserProfile {
// Return the updated profile // Return the updated profile
let metadata = Metadata::from_json(&event.content).unwrap_or_default(); let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata); let profile = Person::new(event.pubkey, metadata);
Ok(profile) Ok(profile)
}) })

View File

@@ -1,6 +1,6 @@
use std::time::Duration; use std::time::Duration;
use common::{nip05_verify, shorten_pubkey, RenderedProfile}; use common::{nip05_verify, shorten_pubkey};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement, div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
@@ -8,8 +8,7 @@ use gpui::{
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::{Person, PersonRegistry};
use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -23,7 +22,7 @@ pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<
#[derive(Debug)] #[derive(Debug)]
pub struct ProfileViewer { pub struct ProfileViewer {
profile: Profile, profile: Person,
/// Follow status /// Follow status
followed: bool, followed: bool,
@@ -134,7 +133,6 @@ impl ProfileViewer {
impl Render for ProfileViewer { impl Render for ProfileViewer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let bech32 = shorten_pubkey(self.profile.public_key(), 16); let bech32 = shorten_pubkey(self.profile.public_key(), 16);
let shared_bech32 = SharedString::from(bech32); let shared_bech32 = SharedString::from(bech32);
@@ -147,14 +145,14 @@ impl Render for ProfileViewer {
.items_center() .items_center()
.justify_center() .justify_center()
.text_center() .text_center()
.child(Avatar::new(self.profile.avatar(proxy)).size(rems(4.))) .child(Avatar::new(self.profile.avatar()).size(rems(4.)))
.child( .child(
v_flex() v_flex()
.child( .child(
div() div()
.font_semibold() .font_semibold()
.line_height(relative(1.25)) .line_height(relative(1.25))
.child(self.profile.display_name()), .child(self.profile.name()),
) )
.when_some(self.address(cx), |this, address| { .when_some(self.address(cx), |this, address| {
this.child( this.child(

View File

@@ -3,7 +3,7 @@ use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use chat::{ChatRegistry, Room}; use chat::{ChatRegistry, Room};
use common::{nip05_profile, RenderedProfile, TextUtils, BOOTSTRAP_RELAYS}; use common::{nip05_profile, TextUtils, BOOTSTRAP_RELAYS};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement, div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
@@ -13,7 +13,6 @@ use gpui::{
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -359,7 +358,6 @@ impl Compose {
} }
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> { fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let mut items = Vec::with_capacity(self.contacts.read(cx).len()); let mut items = Vec::with_capacity(self.contacts.read(cx).len());
@@ -383,8 +381,8 @@ impl Compose {
h_flex() h_flex()
.gap_1p5() .gap_1p5()
.text_sm() .text_sm()
.child(Avatar::new(profile.avatar(proxy)).size(rems(1.75))) .child(Avatar::new(profile.avatar()).size(rems(1.75)))
.child(profile.display_name()), .child(profile.name()),
) )
.when(contact.selected, |this| { .when(contact.selected, |this| {
this.child( this.child(

View File

@@ -8,8 +8,7 @@ use gpui::{
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::{Person, PersonRegistry};
use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -23,7 +22,7 @@ pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<
} }
pub struct Screening { pub struct Screening {
profile: Profile, profile: Person,
verified: bool, verified: bool,
followed: bool, followed: bool,
last_active: Option<Timestamp>, last_active: Option<Timestamp>,
@@ -225,7 +224,6 @@ impl Screening {
impl Render for Screening { impl Render for Screening {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let shorten_pubkey = shorten_pubkey(self.profile.public_key(), 8); let shorten_pubkey = shorten_pubkey(self.profile.public_key(), 8);
let total_mutuals = self.mutual_contacts.len(); let total_mutuals = self.mutual_contacts.len();
let last_active = self.last_active.map(|_| true); let last_active = self.last_active.map(|_| true);
@@ -238,12 +236,12 @@ impl Render for Screening {
.items_center() .items_center()
.justify_center() .justify_center()
.text_center() .text_center()
.child(Avatar::new(self.profile.avatar(proxy)).size(rems(4.))) .child(Avatar::new(self.profile.avatar()).size(rems(4.)))
.child( .child(
div() div()
.font_semibold() .font_semibold()
.line_height(relative(1.25)) .line_height(relative(1.25))
.child(self.profile.display_name()), .child(self.profile.name()),
), ),
) )
.child( .child(

View File

@@ -1,6 +1,6 @@
use std::time::Duration; use std::time::Duration;
use common::{RenderedProfile, BUNKER_TIMEOUT}; use common::BUNKER_TIMEOUT;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
@@ -266,8 +266,8 @@ impl Render for Startup {
) )
}) })
.when(!self.loading, |this| { .when(!self.loading, |this| {
let avatar = profile.avatar(true); let avatar = profile.avatar();
let name = profile.display_name(); let name = profile.name();
this.child( this.child(
h_flex() h_flex()

View File

@@ -7,9 +7,12 @@ use anyhow::{anyhow, Error};
use common::{EventUtils, BOOTSTRAP_RELAYS}; use common::{EventUtils, BOOTSTRAP_RELAYS};
use gpui::{App, AppContext, Context, Entity, Global, Task}; use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
pub use person::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, TIMEOUT}; use state::{NostrRegistry, TIMEOUT};
mod person;
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx); PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
} }
@@ -22,7 +25,7 @@ impl Global for GlobalPersonRegistry {}
#[derive(Debug)] #[derive(Debug)]
pub struct PersonRegistry { pub struct PersonRegistry {
/// Collection of all persons (user profiles) /// Collection of all persons (user profiles)
persons: HashMap<PublicKey, Entity<Profile>>, persons: HashMap<PublicKey, Entity<Person>>,
/// Set of public keys that have been seen /// Set of public keys that have been seen
seen: Rc<RefCell<HashSet<PublicKey>>>, seen: Rc<RefCell<HashSet<PublicKey>>>,
@@ -51,7 +54,7 @@ impl PersonRegistry {
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
// Channel for communication between nostr and gpui // Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Profile>(100); let (tx, rx) = flume::bounded::<Person>(100);
let (mta_tx, mta_rx) = flume::bounded::<PublicKey>(100); let (mta_tx, mta_rx) = flume::bounded::<PublicKey>(100);
let mut tasks = smallvec![]; let mut tasks = smallvec![];
@@ -81,9 +84,9 @@ impl PersonRegistry {
tasks.push( tasks.push(
// Update GPUI state // Update GPUI state
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
while let Ok(profile) = rx.recv_async().await { while let Ok(person) = rx.recv_async().await {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.insert(profile, cx); this.insert(person, cx);
}) })
.ok(); .ok();
} }
@@ -99,9 +102,9 @@ impl PersonRegistry {
.await; .await;
match result { match result {
Ok(profiles) => { Ok(persons) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.bulk_inserts(profiles, cx); this.bulk_inserts(persons, cx);
}) })
.ok(); .ok();
} }
@@ -121,7 +124,7 @@ impl PersonRegistry {
} }
/// Handle nostr notifications /// Handle nostr notifications
async fn handle_notifications(client: &Client, tx: &flume::Sender<Profile>) { async fn handle_notifications(client: &Client, tx: &flume::Sender<Person>) {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
let mut processed_events = HashSet::new(); let mut processed_events = HashSet::new();
@@ -140,9 +143,9 @@ impl PersonRegistry {
match event.kind { match event.kind {
Kind::Metadata => { Kind::Metadata => {
let metadata = Metadata::from_json(&event.content).unwrap_or_default(); let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata); let person = Person::new(event.pubkey, metadata);
tx.send_async(profile).await.ok(); tx.send_async(person).await.ok();
} }
Kind::ContactList => { Kind::ContactList => {
let public_keys = event.extract_public_keys(); let public_keys = event.extract_public_keys();
@@ -214,51 +217,50 @@ impl PersonRegistry {
} }
/// Load all user profiles from the database /// Load all user profiles from the database
async fn load_persons(client: &Client) -> Result<Vec<Profile>, Error> { async fn load_persons(client: &Client) -> Result<Vec<Person>, Error> {
let filter = Filter::new().kind(Kind::Metadata).limit(200); let filter = Filter::new().kind(Kind::Metadata).limit(200);
let events = client.database().query(filter).await?; let events = client.database().query(filter).await?;
let mut profiles = vec![]; let mut persons = vec![];
for event in events.into_iter() { for event in events.into_iter() {
let metadata = Metadata::from_json(event.content).unwrap_or_default(); let metadata = Metadata::from_json(event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata); let person = Person::new(event.pubkey, metadata);
profiles.push(profile); persons.push(person);
} }
Ok(profiles) Ok(persons)
} }
/// Insert batch of persons /// Insert batch of persons
fn bulk_inserts(&mut self, profiles: Vec<Profile>, cx: &mut Context<Self>) { fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
for profile in profiles.into_iter() { for person in persons.into_iter() {
self.persons self.persons.insert(person.public_key(), cx.new(|_| person));
.insert(profile.public_key(), cx.new(|_| profile));
} }
cx.notify(); cx.notify();
} }
/// Insert or update a person /// Insert or update a person
pub fn insert(&mut self, profile: Profile, cx: &mut App) { pub fn insert(&mut self, person: Person, cx: &mut App) {
let public_key = profile.public_key(); let public_key = person.public_key();
match self.persons.get(&public_key) { match self.persons.get(&public_key) {
Some(person) => { Some(this) => {
person.update(cx, |this, cx| { this.update(cx, |this, cx| {
*this = profile; *this = person;
cx.notify(); cx.notify();
}); });
} }
None => { None => {
self.persons.insert(public_key, cx.new(|_| profile)); self.persons.insert(public_key, cx.new(|_| person));
} }
} }
} }
/// Get single person by public key /// Get single person by public key
pub fn get(&self, public_key: &PublicKey, cx: &App) -> Profile { pub fn get(&self, public_key: &PublicKey, cx: &App) -> Person {
if let Some(profile) = self.persons.get(public_key) { if let Some(person) = self.persons.get(public_key) {
return profile.read(cx).clone(); return person.read(cx).clone();
} }
let public_key = *public_key; let public_key = *public_key;
@@ -277,6 +279,6 @@ impl PersonRegistry {
} }
// Return a temporary profile with default metadata // Return a temporary profile with default metadata
Profile::new(public_key, Metadata::default()) Person::new(public_key, Metadata::default())
} }
} }

111
crates/person/src/person.rs Normal file
View File

@@ -0,0 +1,111 @@
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use gpui::SharedString;
use nostr_sdk::prelude::*;
use state::Announcement;
/// Person
#[derive(Debug, Clone)]
pub struct Person {
public_key: PublicKey,
metadata: Metadata,
announcement: Option<Announcement>,
}
impl PartialEq for Person {
fn eq(&self, other: &Self) -> bool {
self.public_key == other.public_key
}
}
impl Eq for Person {}
impl PartialOrd for Person {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Person {
fn cmp(&self, other: &Self) -> Ordering {
self.name().cmp(&other.name())
}
}
impl Hash for Person {
fn hash<H: Hasher>(&self, state: &mut H) {
self.public_key.hash(state)
}
}
impl From<PublicKey> for Person {
fn from(public_key: PublicKey) -> Self {
Self::new(public_key, Metadata::default())
}
}
impl Person {
pub fn new(public_key: PublicKey, metadata: Metadata) -> Self {
Self {
public_key,
metadata,
announcement: None,
}
}
/// Get profile public key
pub fn public_key(&self) -> PublicKey {
self.public_key
}
/// Get profile metadata
pub fn metadata(&self) -> Metadata {
self.metadata.clone()
}
/// Get profile encryption keys announcement
pub fn announcement(&self) -> Option<Announcement> {
self.announcement.clone()
}
/// Get profile avatar
pub fn avatar(&self) -> SharedString {
self.metadata()
.picture
.as_ref()
.filter(|picture| !picture.is_empty())
.map(|picture| picture.into())
.unwrap_or_else(|| "brand/avatar.png".into())
}
/// Get profile name
pub fn name(&self) -> SharedString {
if let Some(display_name) = self.metadata().display_name.as_ref() {
if !display_name.is_empty() {
return SharedString::from(display_name);
}
}
if let Some(name) = self.metadata().name.as_ref() {
if !name.is_empty() {
return SharedString::from(name);
}
}
SharedString::from(shorten_pubkey(self.public_key(), 4))
}
}
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters
///
/// Ex. `00000000:00000002`
pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
let Ok(pubkey) = public_key.to_bech32();
format!(
"{}:{}",
&pubkey[0..(len + 1)],
&pubkey[pubkey.len() - len..]
)
}

View File

@@ -8,6 +8,26 @@ pub struct Announcement {
client_name: Option<String>, client_name: Option<String>,
} }
impl From<&Event> for Announcement {
fn from(val: &Event) -> Self {
let public_key = val
.tags
.iter()
.find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "P")
.and_then(|tag| tag.content())
.and_then(|c| PublicKey::parse(c).ok())
.unwrap_or(val.pubkey);
let client_name = val
.tags
.find(TagKind::Client)
.and_then(|tag| tag.content())
.map(|c| c.to_string());
Self::new(val.id, client_name, public_key)
}
}
impl Announcement { impl Announcement {
pub fn new(id: EventId, client_name: Option<String>, public_key: PublicKey) -> Self { pub fn new(id: EventId, client_name: Option<String>, public_key: PublicKey) -> Self {
Self { Self {

View File

@@ -528,8 +528,18 @@ impl NostrRegistry {
.limit(1) .limit(1)
.author(public_key); .author(public_key);
// Filter for encryption keys announcement
let encryption_keys = Filter::new()
.kind(Kind::Custom(10044))
.limit(1)
.author(public_key);
client client
.subscribe_to(urls, vec![metadata, contact_list], Some(opts)) .subscribe_to(
urls,
vec![metadata, contact_list, encryption_keys],
Some(opts),
)
.await?; .await?;
Ok(()) Ok(())