feat: rewrite the nip-4e implementation #1

Merged
reya merged 17 commits from rewrite-nostr-backend into master 2026-01-13 16:00:09 +08:00
14 changed files with 214 additions and 81 deletions
Showing only changes of commit bf586580ab - Show all commits

View File

@@ -4,11 +4,11 @@ use std::hash::{Hash, Hasher};
use std::time::Duration;
use anyhow::Error;
use common::{EventUtils, RenderedProfile};
use common::EventUtils;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use person::{Person, PersonRegistry};
use state::{tracker, NostrRegistry};
use crate::NewMessage;
@@ -264,9 +264,9 @@ impl 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() {
self.display_member(cx).avatar(proxy)
self.display_member(cx).avatar()
} else {
SharedString::from("brand/group.png")
}
@@ -275,7 +275,7 @@ impl Room {
/// Get a member to represent the room
///
/// 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 nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
@@ -295,7 +295,7 @@ impl Room {
let persons = PersonRegistry::global(cx);
if self.is_group() {
let profiles: Vec<Profile> = self
let profiles: Vec<Person> = self
.members
.iter()
.map(|public_key| persons.read(cx).get(public_key, cx))
@@ -314,7 +314,7 @@ impl Room {
SharedString::from(name)
} 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::*;
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::{
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 itertools::Itertools;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use person::{Person, PersonRegistry};
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
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);
persons.read(cx).get(public_key, cx)
}
@@ -632,7 +632,7 @@ impl ChatPanel {
this.child(
div()
.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| {
let view = Box::new(OpenPublicKey(public_key));
let copy = Box::new(CopyPublicKey(public_key));
@@ -657,7 +657,7 @@ impl ChatPanel {
div()
.font_semibold()
.text_color(cx.theme().text)
.child(author.display_name()),
.child(author.name()),
)
.child(message.created_at.to_human_time())
.when_some(is_sent_success, |this, status| {
@@ -714,7 +714,7 @@ impl ChatPanel {
.child(
div()
.text_color(cx.theme().text_accent)
.child(author.display_name()),
.child(author.name()),
)
.child(
div()
@@ -796,8 +796,8 @@ impl ChatPanel {
fn render_report(report: &SendReport, cx: &App) -> impl IntoElement {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&report.receiver, cx);
let name = profile.display_name();
let avatar = profile.avatar(true);
let name = profile.name();
let avatar = profile.avatar();
v_flex()
.gap_2()
@@ -1080,7 +1080,7 @@ impl ChatPanel {
.child(
div()
.text_color(cx.theme().text_accent)
.child(profile.display_name()),
.child(profile.name()),
),
)
.child(
@@ -1134,7 +1134,7 @@ impl Panel for ChatPanel {
.read_with(cx, |this, cx| {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let label = this.display_name(cx);
let url = this.display_image(proxy, cx);
let url = this.display_image(cx);
h_flex()
.gap_1p5()

View File

@@ -1,7 +1,6 @@
use std::ops::Range;
use std::sync::Arc;
use common::RenderedProfile;
use gpui::{
AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString,
StyledText, UnderlineStyle, Window,
@@ -255,7 +254,7 @@ fn render_pubkey(
) {
let persons = PersonRegistry::global(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);

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use auto_update::{AutoUpdateStatus, AutoUpdater};
use chat::{ChatEvent, ChatRegistry};
use chat_ui::{CopyPublicKey, OpenPublicKey};
use common::{RenderedProfile, DEFAULT_SIDEBAR_WIDTH};
use common::DEFAULT_SIDEBAR_WIDTH;
use gpui::prelude::FluentBuilder;
use gpui::{
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 person::PersonRegistry;
use relay_auth::RelayAuth;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry};
@@ -257,9 +256,9 @@ impl ChatSpace {
this.update_in(cx, |_, window, cx| {
match result {
Ok(profile) => {
Ok(person) => {
persons.update(cx, |this, cx| {
this.insert(profile, cx);
this.insert(person, cx);
// Close the edit profile modal
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 {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let auto_update = AutoUpdater::global(cx);
let relay_auth = RelayAuth::global(cx);
@@ -562,9 +560,9 @@ impl ChatSpace {
.reverse()
.transparent()
.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| {
this.label(profile.display_name())
this.label(profile.name())
.menu_with_icon(
"Profile",
IconName::EmojiFill,

View File

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

View File

@@ -10,6 +10,7 @@ use gpui::{
};
use gpui_tokio::Tokio;
use nostr_sdk::prelude::*;
use person::Person;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use smol::fs;
@@ -233,7 +234,7 @@ impl UserProfile {
.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 name = self.name_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
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)
})

View File

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

View File

@@ -3,7 +3,7 @@ use std::time::Duration;
use anyhow::{anyhow, Error};
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::{
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
@@ -13,7 +13,6 @@ use gpui::{
use gpui_tokio::Tokio;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::ActiveTheme;
@@ -359,7 +358,6 @@ impl Compose {
}
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 mut items = Vec::with_capacity(self.contacts.read(cx).len());
@@ -383,8 +381,8 @@ impl Compose {
h_flex()
.gap_1p5()
.text_sm()
.child(Avatar::new(profile.avatar(proxy)).size(rems(1.75)))
.child(profile.display_name()),
.child(Avatar::new(profile.avatar()).size(rems(1.75)))
.child(profile.name()),
)
.when(contact.selected, |this| {
this.child(

View File

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

View File

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

View File

@@ -7,9 +7,12 @@ use anyhow::{anyhow, Error};
use common::{EventUtils, BOOTSTRAP_RELAYS};
use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_sdk::prelude::*;
pub use person::*;
use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, TIMEOUT};
mod person;
pub fn init(cx: &mut App) {
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
}
@@ -22,7 +25,7 @@ impl Global for GlobalPersonRegistry {}
#[derive(Debug)]
pub struct PersonRegistry {
/// Collection of all persons (user profiles)
persons: HashMap<PublicKey, Entity<Profile>>,
persons: HashMap<PublicKey, Entity<Person>>,
/// Set of public keys that have been seen
seen: Rc<RefCell<HashSet<PublicKey>>>,
@@ -51,7 +54,7 @@ impl PersonRegistry {
let client = nostr.read(cx).client();
// 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 mut tasks = smallvec![];
@@ -81,9 +84,9 @@ impl PersonRegistry {
tasks.push(
// Update GPUI state
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.insert(profile, cx);
this.insert(person, cx);
})
.ok();
}
@@ -99,9 +102,9 @@ impl PersonRegistry {
.await;
match result {
Ok(profiles) => {
Ok(persons) => {
this.update(cx, |this, cx| {
this.bulk_inserts(profiles, cx);
this.bulk_inserts(persons, cx);
})
.ok();
}
@@ -121,7 +124,7 @@ impl PersonRegistry {
}
/// 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 processed_events = HashSet::new();
@@ -140,9 +143,9 @@ impl PersonRegistry {
match event.kind {
Kind::Metadata => {
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 => {
let public_keys = event.extract_public_keys();
@@ -214,51 +217,50 @@ impl PersonRegistry {
}
/// 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 events = client.database().query(filter).await?;
let mut profiles = vec![];
let mut persons = vec![];
for event in events.into_iter() {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
profiles.push(profile);
let person = Person::new(event.pubkey, metadata);
persons.push(person);
}
Ok(profiles)
Ok(persons)
}
/// Insert batch of persons
fn bulk_inserts(&mut self, profiles: Vec<Profile>, cx: &mut Context<Self>) {
for profile in profiles.into_iter() {
self.persons
.insert(profile.public_key(), cx.new(|_| profile));
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
for person in persons.into_iter() {
self.persons.insert(person.public_key(), cx.new(|_| person));
}
cx.notify();
}
/// Insert or update a person
pub fn insert(&mut self, profile: Profile, cx: &mut App) {
let public_key = profile.public_key();
pub fn insert(&mut self, person: Person, cx: &mut App) {
let public_key = person.public_key();
match self.persons.get(&public_key) {
Some(person) => {
person.update(cx, |this, cx| {
*this = profile;
Some(this) => {
this.update(cx, |this, cx| {
*this = person;
cx.notify();
});
}
None => {
self.persons.insert(public_key, cx.new(|_| profile));
self.persons.insert(public_key, cx.new(|_| person));
}
}
}
/// Get single person by public key
pub fn get(&self, public_key: &PublicKey, cx: &App) -> Profile {
if let Some(profile) = self.persons.get(public_key) {
return profile.read(cx).clone();
pub fn get(&self, public_key: &PublicKey, cx: &App) -> Person {
if let Some(person) = self.persons.get(public_key) {
return person.read(cx).clone();
}
let public_key = *public_key;
@@ -277,6 +279,6 @@ impl PersonRegistry {
}
// 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>,
}
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 {
pub fn new(id: EventId, client_name: Option<String>, public_key: PublicKey) -> Self {
Self {

View File

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