feat: bind user metadata to per-view directories (#64)
Reviewed-on: #64
This commit was merged in pull request #64.
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use anyhow::Error;
|
||||
use common::emojis_from_tags;
|
||||
use gpui::{App, AppContext, Context, Entity, Subscription, Task};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
|
||||
use crate::{Person, PersonEvent, PersonRegistry};
|
||||
|
||||
/// Directory of persons tracked by a view
|
||||
#[derive(Debug)]
|
||||
pub struct Directory {
|
||||
persons: HashMap<PublicKey, Person>,
|
||||
/// Keys awaiting their first update, guarded for render-time access
|
||||
pending: RwLock<HashSet<PublicKey>>,
|
||||
tasks: SmallVec<[Task<()>; 2]>,
|
||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
/// Create a directory following the registry's events
|
||||
pub fn new(cx: &mut App) -> Entity<Self> {
|
||||
let registry = PersonRegistry::global(cx);
|
||||
|
||||
cx.new(|cx| {
|
||||
let subscription = cx.subscribe(®istry, |this: &mut Self, _registry, event, cx| {
|
||||
this.handle_event(event.clone(), cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
persons: HashMap::new(),
|
||||
pending: RwLock::new(HashSet::new()),
|
||||
tasks: SmallVec::new(),
|
||||
_subscriptions: smallvec![subscription],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Load known profiles from the database, request the rest
|
||||
pub fn track(&mut self, keys: impl IntoIterator<Item = PublicKey>, cx: &mut Context<Self>) {
|
||||
/// Profiles and emoji lists loaded from the database
|
||||
type Loaded = (Vec<Person>, Vec<(PublicKey, Vec<(String, Url)>)>);
|
||||
|
||||
let missing: Vec<PublicKey> = keys
|
||||
.into_iter()
|
||||
.filter(|key| {
|
||||
!self.persons.contains_key(key)
|
||||
&& !self.with_pending(|pending| pending.contains(key))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if missing.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Accept events that arrive while the database query is running
|
||||
self.with_pending(|pending| {
|
||||
pending.extend(missing.iter().copied());
|
||||
});
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let authors = missing.clone();
|
||||
|
||||
let task: Task<Result<Loaded, Error>> = cx.background_spawn(async move {
|
||||
let mut metadata_events: Vec<Event> = client
|
||||
.database()
|
||||
.query(Filter::new().kind(Kind::Metadata).authors(authors.clone()))
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
// Keep only the newest event per author
|
||||
metadata_events.sort_by_key(|event| Reverse(event.created_at));
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
let persons = metadata_events
|
||||
.into_iter()
|
||||
.filter(|event| seen.insert(event.pubkey))
|
||||
.map(|event| {
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
Person::new(event.pubkey, metadata)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut emoji_events: Vec<Event> = client
|
||||
.database()
|
||||
.query(Filter::new().kind(Kind::Emojis).authors(authors))
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
// Keep only the newest event per author
|
||||
emoji_events.sort_by_key(|event| Reverse(event.created_at));
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
let emojis = emoji_events
|
||||
.into_iter()
|
||||
.filter(|event| seen.insert(event.pubkey))
|
||||
.map(|event| (event.pubkey, emojis_from_tags(&event.tags)))
|
||||
.collect();
|
||||
|
||||
Ok((persons, emojis))
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let loaded = task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let remaining = match loaded {
|
||||
Ok((persons, emojis)) => {
|
||||
let found: HashSet<PublicKey> =
|
||||
persons.iter().map(Person::public_key).collect();
|
||||
this.bulk_insert(persons, cx);
|
||||
|
||||
// Emojis apply on top of loaded or already received data
|
||||
let mut updated = false;
|
||||
|
||||
for (public_key, emojis) in emojis {
|
||||
if emojis.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let person = this
|
||||
.persons
|
||||
.entry(public_key)
|
||||
.or_insert_with(|| Person::new(public_key, Metadata::default()));
|
||||
person.set_emojis(emojis);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if updated {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
missing
|
||||
.into_iter()
|
||||
.filter(|key| !found.contains(key))
|
||||
.collect()
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load persons from the database: {e}");
|
||||
missing
|
||||
}
|
||||
};
|
||||
|
||||
if !remaining.is_empty() {
|
||||
let registry = PersonRegistry::global(cx);
|
||||
registry.read(cx).fetch(remaining);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
/// The person for a key, or a placeholder that queues a request
|
||||
pub fn person(&self, public_key: &PublicKey, cx: &App) -> Person {
|
||||
if let Some(person) = self.persons.get(public_key) {
|
||||
return person.clone();
|
||||
}
|
||||
|
||||
if self.with_pending(|pending| pending.insert(*public_key)) {
|
||||
let registry = PersonRegistry::global(cx);
|
||||
registry.read(cx).fetch([*public_key]);
|
||||
}
|
||||
|
||||
Person::new(*public_key, Metadata::default())
|
||||
}
|
||||
|
||||
/// Merge event data into the tracked person
|
||||
fn handle_event(&mut self, event: PersonEvent, cx: &mut Context<Self>) {
|
||||
let public_key = match &event {
|
||||
PersonEvent::Metadata(person) => person.public_key(),
|
||||
PersonEvent::Emojis { public_key, .. }
|
||||
| PersonEvent::Announcement { public_key, .. }
|
||||
| PersonEvent::MessagingRelays { public_key, .. } => *public_key,
|
||||
};
|
||||
|
||||
if !self.persons.contains_key(&public_key)
|
||||
&& !self.with_pending(|pending| pending.remove(&public_key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let person = self
|
||||
.persons
|
||||
.entry(public_key)
|
||||
.or_insert_with(|| Person::new(public_key, Metadata::default()));
|
||||
|
||||
match event {
|
||||
PersonEvent::Metadata(updated) => person.set_metadata(updated.metadata()),
|
||||
PersonEvent::Emojis { emojis, .. } => person.set_emojis(emojis),
|
||||
PersonEvent::Announcement { announcement, .. } => {
|
||||
person.set_announcement(announcement);
|
||||
}
|
||||
PersonEvent::MessagingRelays { relays, .. } => person.set_messaging_relays(relays),
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Insert loaded persons, keeping data already received from relays
|
||||
fn bulk_insert(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
||||
let mut inserted = false;
|
||||
|
||||
for person in persons {
|
||||
let public_key = person.public_key();
|
||||
|
||||
if self.persons.contains_key(&public_key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.persons.insert(public_key, person);
|
||||
inserted = true;
|
||||
}
|
||||
|
||||
if inserted {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `then` with the pending set, tolerating poisoning: it is only a dedupe cache
|
||||
fn with_pending<R>(&self, then: impl FnOnce(&mut HashSet<PublicKey>) -> R) -> R {
|
||||
let mut pending = match self.pending.write() {
|
||||
Ok(pending) => pending,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
then(&mut pending)
|
||||
}
|
||||
}
|
||||
+79
-186
@@ -1,17 +1,18 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use anyhow::{Error, anyhow};
|
||||
use common::{EventExt, emojis_from_tags};
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
||||
use common::emojis_from_tags;
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task};
|
||||
use instant::Duration;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
|
||||
|
||||
mod directory;
|
||||
mod person;
|
||||
|
||||
pub use directory::*;
|
||||
pub use person::*;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -22,6 +23,30 @@ struct GlobalPersonRegistry(Entity<PersonRegistry>);
|
||||
|
||||
impl Global for GlobalPersonRegistry {}
|
||||
|
||||
/// A change to a person's data
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PersonEvent {
|
||||
/// Profile metadata
|
||||
Metadata(Box<Person>),
|
||||
/// Custom emoji list (NIP-30)
|
||||
Emojis {
|
||||
public_key: PublicKey,
|
||||
emojis: Vec<(String, Url)>,
|
||||
},
|
||||
/// Profile encryption keys announcement (NIP-4e)
|
||||
Announcement {
|
||||
public_key: PublicKey,
|
||||
announcement: Announcement,
|
||||
},
|
||||
/// Messaging relay list (NIP-65)
|
||||
MessagingRelays {
|
||||
public_key: PublicKey,
|
||||
relays: Vec<RelayUrl>,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventEmitter<PersonEvent> for PersonRegistry {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Dispatch {
|
||||
Person(Person),
|
||||
@@ -33,10 +58,8 @@ enum Dispatch {
|
||||
/// Person Registry
|
||||
#[derive(Debug)]
|
||||
pub struct PersonRegistry {
|
||||
/// Collection of all persons (user profiles)
|
||||
persons: HashMap<PublicKey, Entity<Person>>,
|
||||
/// Set of public keys that have been seen
|
||||
seen: RwLock<HashSet<PublicKey>>,
|
||||
/// Public keys with a metadata request in flight
|
||||
in_flight: RwLock<HashSet<PublicKey>>,
|
||||
sender: flume::Sender<PublicKey>,
|
||||
tasks: SmallVec<[Task<()>; 4]>,
|
||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||
@@ -57,7 +80,6 @@ impl PersonRegistry {
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let entity = cx.entity().downgrade();
|
||||
|
||||
// Channel for communication between nostr and gpui
|
||||
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
||||
@@ -103,17 +125,8 @@ impl PersonRegistry {
|
||||
}
|
||||
});
|
||||
|
||||
// Load all user profiles from the database
|
||||
cx.defer(move |cx| {
|
||||
_ = entity.update(cx, |this, cx| {
|
||||
this.load(cx);
|
||||
this.load_emojis(cx);
|
||||
});
|
||||
});
|
||||
|
||||
Self {
|
||||
persons: HashMap::new(),
|
||||
seen: RwLock::new(HashSet::new()),
|
||||
in_flight: RwLock::new(HashSet::new()),
|
||||
sender: metadata_tx,
|
||||
tasks,
|
||||
_subscriptions: smallvec![subscription],
|
||||
@@ -146,12 +159,6 @@ impl PersonRegistry {
|
||||
log::warn!("Channel closed, dropping metadata event");
|
||||
}
|
||||
}
|
||||
Kind::ContactList => {
|
||||
let public_keys = event.extract_public_keys();
|
||||
if let Err(e) = get_metadata(client, public_keys).await {
|
||||
log::warn!("Failed to get metadata for contact list: {e}");
|
||||
}
|
||||
}
|
||||
Kind::InboxRelays => {
|
||||
_ = tx.send_async(Dispatch::Relays(event.into_owned())).await;
|
||||
}
|
||||
@@ -198,72 +205,6 @@ impl PersonRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all user profiles from the database
|
||||
fn load(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
let task: Task<Result<Vec<Person>, Error>> = cx.background_spawn(async move {
|
||||
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||
let events = client.database().query(filter).await?;
|
||||
let persons = events
|
||||
.into_iter()
|
||||
.map(|event| {
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
Person::new(event.pubkey, metadata)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(persons)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Ok(persons) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.bulk_insert(persons, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Load cached emoji lists from the database
|
||||
fn load_emojis(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
type EmojiLists = Vec<(PublicKey, Vec<(String, Url)>)>;
|
||||
|
||||
let task: Task<Result<EmojiLists, Error>> = cx.background_spawn(async move {
|
||||
let filter = Filter::new().kind(Kind::Emojis).limit(200);
|
||||
let mut events: Vec<Event> =
|
||||
client.database().query(filter).await?.into_iter().collect();
|
||||
|
||||
// Keep only the newest list per author
|
||||
events.sort_by_key(|event| Reverse(event.created_at));
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
let lists = events
|
||||
.into_iter()
|
||||
.filter(|event| seen.insert(event.pubkey))
|
||||
.map(|event| (event.pubkey, emojis_from_tags(&event.tags)))
|
||||
.collect();
|
||||
|
||||
Ok(lists)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Ok(lists) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
for (public_key, emojis) in lists {
|
||||
this.set_emojis_for(public_key, emojis, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Subscribe to the signed-in user's emoji list
|
||||
fn get_emojis(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
@@ -283,130 +224,82 @@ impl PersonRegistry {
|
||||
}
|
||||
|
||||
/// Set profile encryption keys announcement
|
||||
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
|
||||
fn set_announcement(&mut self, event: &Event, cx: &mut Context<Self>) {
|
||||
let Some(announcement) = Announcement::from_event(event) else {
|
||||
log::warn!("Ignoring announcement with missing or invalid key tag");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
person.update(cx, |person, cx| {
|
||||
person.set_announcement(announcement);
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
let person =
|
||||
Person::new(event.pubkey, Metadata::default()).with_announcement(announcement);
|
||||
self.insert(person, cx);
|
||||
}
|
||||
cx.emit(PersonEvent::Announcement {
|
||||
public_key: event.pubkey,
|
||||
announcement,
|
||||
});
|
||||
}
|
||||
|
||||
/// Set messaging relays for a person
|
||||
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
|
||||
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).collect();
|
||||
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
person.update(cx, |person, cx| {
|
||||
person.set_messaging_relays(urls);
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls);
|
||||
self.insert(person, cx);
|
||||
}
|
||||
fn set_messaging_relays(&mut self, event: &Event, cx: &mut Context<Self>) {
|
||||
cx.emit(PersonEvent::MessagingRelays {
|
||||
public_key: event.pubkey,
|
||||
relays: nip17::extract_relay_list(event).collect(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Set custom emoji for a person from their emoji list event
|
||||
fn set_emojis(&mut self, event: &Event, cx: &mut App) {
|
||||
fn set_emojis(&mut self, event: &Event, cx: &mut Context<Self>) {
|
||||
self.set_emojis_for(event.pubkey, emojis_from_tags(&event.tags), cx);
|
||||
}
|
||||
|
||||
/// Set custom emoji for a person
|
||||
fn set_emojis_for(&mut self, public_key: PublicKey, emojis: Vec<(String, Url)>, cx: &mut App) {
|
||||
if let Some(person) = self.persons.get(&public_key) {
|
||||
person.update(cx, |person, cx| {
|
||||
person.set_emojis(emojis);
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
let person = Person::new(public_key, Metadata::default()).with_emojis(emojis);
|
||||
self.insert(person, cx);
|
||||
}
|
||||
fn set_emojis_for(
|
||||
&mut self,
|
||||
public_key: PublicKey,
|
||||
emojis: Vec<(String, Url)>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
cx.emit(PersonEvent::Emojis { public_key, emojis });
|
||||
}
|
||||
|
||||
/// Insert batch of persons
|
||||
fn bulk_insert(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
||||
for person in persons.into_iter() {
|
||||
let public_key = person.public_key();
|
||||
self.persons
|
||||
.entry(public_key)
|
||||
.or_insert_with(|| cx.new(|_| person));
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Insert or update a person
|
||||
pub fn insert(&mut self, person: Person, cx: &mut App) {
|
||||
/// Publish a person's profile metadata to every directory
|
||||
pub fn insert(&mut self, person: Person, cx: &mut Context<Self>) {
|
||||
let public_key = person.public_key();
|
||||
self.release_in_flight(&public_key);
|
||||
|
||||
match self.persons.get(&public_key) {
|
||||
Some(this) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_metadata(person.metadata());
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
None => {
|
||||
self.persons.insert(public_key, cx.new(|_| person));
|
||||
}
|
||||
}
|
||||
cx.emit(PersonEvent::Metadata(Box::new(person)));
|
||||
}
|
||||
|
||||
/// Get single person by public key
|
||||
pub fn get(&self, public_key: &PublicKey, cx: &App) -> Person {
|
||||
if let Some(person) = self.persons.get(public_key) {
|
||||
return person.read(cx).clone();
|
||||
}
|
||||
/// Queue a batched metadata request for keys without one in flight
|
||||
pub(crate) fn fetch<I>(&self, keys: I)
|
||||
where
|
||||
I: IntoIterator<Item = PublicKey>,
|
||||
{
|
||||
let mut queue = Vec::new();
|
||||
{
|
||||
let mut in_flight = match self.in_flight.write() {
|
||||
Ok(in_flight) => in_flight,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
|
||||
let public_key = *public_key;
|
||||
|
||||
if self.seen.write().unwrap().insert(public_key) {
|
||||
let sender = self.sender.clone();
|
||||
|
||||
// Spawn background task to request metadata
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = sender.send_async(public_key).await {
|
||||
log::warn!("Failed to send public key for metadata request: {e}");
|
||||
for key in keys {
|
||||
if in_flight.insert(key) {
|
||||
queue.push(key);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
// Return a temporary profile with default metadata
|
||||
Person::new(public_key, Metadata::default())
|
||||
for public_key in queue {
|
||||
if let Err(error) = self.sender.send(public_key) {
|
||||
log::warn!("Failed to queue metadata request: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The signed-in user's custom emoji
|
||||
pub fn current_emojis(cx: &App) -> Vec<(String, Url)> {
|
||||
let registry = Self::global(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
|
||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||
return Vec::new();
|
||||
/// Allow future fetches for a key once its metadata has arrived
|
||||
fn release_in_flight(&self, public_key: &PublicKey) {
|
||||
let mut in_flight = match self.in_flight.write() {
|
||||
Ok(in_flight) => in_flight,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
|
||||
registry.read(cx).get(&public_key, cx).emojis().to_vec()
|
||||
}
|
||||
|
||||
/// An `emoji` tag for every signed-in user's custom emoji referenced.
|
||||
pub fn emoji_tags(content: &str, cx: &App) -> Vec<Tag> {
|
||||
let registry = Self::global(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
registry.read(cx).get(&public_key, cx).emoji_tags(content)
|
||||
in_flight.remove(public_key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,21 +57,6 @@ impl Person {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build profile encryption keys announcement
|
||||
pub(crate) fn with_announcement(mut self, announcement: Announcement) -> Self {
|
||||
self.announcement = Some(announcement);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build profile messaging relays
|
||||
pub(crate) fn with_messaging_relays<I>(mut self, relays: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
self.messaging_relays = relays.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Get profile public key
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
self.public_key
|
||||
@@ -92,12 +77,6 @@ impl Person {
|
||||
self.emojis = emojis;
|
||||
}
|
||||
|
||||
/// Build custom emoji from the person's emoji list
|
||||
pub(crate) fn with_emojis(mut self, emojis: Vec<(String, Url)>) -> Self {
|
||||
self.emojis = emojis;
|
||||
self
|
||||
}
|
||||
|
||||
/// An `emoji` tag for every custom emoji referenced as `:shortcode:` in `content`
|
||||
pub fn emoji_tags(&self, content: &str) -> Vec<Tag> {
|
||||
let mut tags = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user