Compare commits
2
Commits
master
...
96ceacb5fb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96ceacb5fb | ||
|
|
c058270eff |
@@ -0,0 +1,158 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use gpui::Context;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
use crate::protocol::{GroupCandidate, GroupId, GroupList};
|
||||||
|
|
||||||
|
/// The groups discovered from the cache and the relays.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Discovery {
|
||||||
|
/// The (relay, group) references of every cached group list.
|
||||||
|
refs: HashMap<RelayUrl, HashSet<GroupId>>,
|
||||||
|
/// The groups whose metadata has resolved so far.
|
||||||
|
discovered: Vec<GroupCandidate>,
|
||||||
|
/// The in-flight preview subscriptions, keyed by subscription id.
|
||||||
|
previews: HashMap<SubscriptionId, RelayUrl>,
|
||||||
|
/// Whether the contacts' group lists have been answered.
|
||||||
|
lists_settled: bool,
|
||||||
|
/// Whether the pool-wide metadata discovery has been answered.
|
||||||
|
sweep_settled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Discovery {
|
||||||
|
pub fn discovered(&self) -> &[GroupCandidate] {
|
||||||
|
&self.discovered
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loading(&self) -> bool {
|
||||||
|
!self.lists_settled || !self.sweep_settled || !self.previews.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the contacts' group lists have been answered.
|
||||||
|
pub(crate) fn lists_settled(&self) -> bool {
|
||||||
|
self.lists_settled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The discovery is starting over: nothing has answered yet.
|
||||||
|
pub(crate) fn begin(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.lists_settled = false;
|
||||||
|
self.sweep_settled = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// There is nothing on the way: there is nothing to discover.
|
||||||
|
pub(crate) fn settle_all(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.lists_settled = true;
|
||||||
|
self.sweep_settled = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contacts' group lists have been answered.
|
||||||
|
pub(crate) fn settle_lists(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.lists_settled = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pool-wide metadata discovery has been answered.
|
||||||
|
pub(crate) fn settle_sweep(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.sweep_settled = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Absorb the references of a group list, skipping the joined groups.
|
||||||
|
///
|
||||||
|
/// Returns whether new references were added.
|
||||||
|
pub(crate) fn absorb_list(
|
||||||
|
&mut self,
|
||||||
|
list: &GroupList,
|
||||||
|
joined: &HashSet<(RelayUrl, GroupId)>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> bool {
|
||||||
|
let mut added = false;
|
||||||
|
|
||||||
|
for entry in list.entries() {
|
||||||
|
if joined.contains(&(entry.relay.clone(), entry.id.clone())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.refs
|
||||||
|
.entry(entry.relay.clone())
|
||||||
|
.or_default()
|
||||||
|
.insert(entry.id.clone())
|
||||||
|
{
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if added {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
added
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The (relay, group) references, for asking the relays for metadata.
|
||||||
|
pub(crate) fn refs(&self) -> &HashMap<RelayUrl, HashSet<GroupId>> {
|
||||||
|
&self.refs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a preview subscription is already asking this relay.
|
||||||
|
pub(crate) fn preview_in_flight(&self, relay: &RelayUrl) -> bool {
|
||||||
|
self.previews.values().any(|in_flight| in_flight == relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The relay a preview subscription is asking, if it is in flight.
|
||||||
|
pub(crate) fn preview_relay(&self, id: &SubscriptionId) -> Option<RelayUrl> {
|
||||||
|
self.previews.get(id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ids of the in-flight preview subscriptions.
|
||||||
|
pub(crate) fn preview_ids(&self) -> Vec<SubscriptionId> {
|
||||||
|
self.previews.keys().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A preview subscription starts asking a relay for metadata.
|
||||||
|
pub(crate) fn track_preview(&mut self, id: SubscriptionId, relay: RelayUrl) {
|
||||||
|
self.previews.insert(id, relay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A preview subscription has been answered.
|
||||||
|
///
|
||||||
|
/// Returns whether it was in flight.
|
||||||
|
pub(crate) fn settle_preview(&mut self, id: &SubscriptionId, cx: &mut Context<Self>) -> bool {
|
||||||
|
let removed = self.previews.remove(id).is_some();
|
||||||
|
|
||||||
|
if removed {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add or replace a discovered group.
|
||||||
|
pub(crate) fn upsert(&mut self, candidate: GroupCandidate, cx: &mut Context<Self>) {
|
||||||
|
match self.discovered.iter_mut().find(|d| d.key == candidate.key) {
|
||||||
|
Some(existing) => *existing = candidate,
|
||||||
|
None => self.discovered.push(candidate),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.discovered.sort_by(|a, b| {
|
||||||
|
a.display_name()
|
||||||
|
.to_lowercase()
|
||||||
|
.cmp(&b.display_name().to_lowercase())
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reset(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.refs.clear();
|
||||||
|
self.discovered.clear();
|
||||||
|
self.previews.clear();
|
||||||
|
self.lists_settled = false;
|
||||||
|
self.sweep_settled = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
+525
-6
@@ -10,9 +10,11 @@ use nostr_sdk::prelude::*;
|
|||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
|
|
||||||
|
mod discovery;
|
||||||
mod group;
|
mod group;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
|
|
||||||
|
pub use discovery::Discovery;
|
||||||
pub use group::*;
|
pub use group::*;
|
||||||
pub use protocol::*;
|
pub use protocol::*;
|
||||||
|
|
||||||
@@ -20,8 +22,11 @@ pub(crate) static LOCAL_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
|
|||||||
|
|
||||||
const LIST_SUBSCRIPTION: &str = "nip29-list";
|
const LIST_SUBSCRIPTION: &str = "nip29-list";
|
||||||
const FORK_SUBSCRIPTION: &str = "nip29-forks";
|
const FORK_SUBSCRIPTION: &str = "nip29-forks";
|
||||||
|
const CONTACTS_SUBSCRIPTION: &str = "nip29-contacts";
|
||||||
|
const DISCOVERY_SUBSCRIPTION: &str = "nip29-discovery";
|
||||||
const GROUP_PREFIX: &str = "grp-";
|
const GROUP_PREFIX: &str = "grp-";
|
||||||
const BROWSE_PREFIX: &str = "brw-";
|
const BROWSE_PREFIX: &str = "brw-";
|
||||||
|
const PREVIEW_PREFIX: &str = "prv-";
|
||||||
const PAGE_PREFIX: &str = "pg-";
|
const PAGE_PREFIX: &str = "pg-";
|
||||||
const BROWSE_LIMIT: usize = 500;
|
const BROWSE_LIMIT: usize = 500;
|
||||||
const BROWSE_TIMEOUT: Duration = Duration::from_secs(10);
|
const BROWSE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
@@ -45,10 +50,13 @@ pub enum GroupsEvent {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Signal {
|
enum Signal {
|
||||||
Event(SubscriptionId, Event),
|
Event(SubscriptionId, RelayUrl, Box<Event>),
|
||||||
Eose(SubscriptionId),
|
Eose(SubscriptionId),
|
||||||
Closed(SubscriptionId, String),
|
Closed(SubscriptionId, String),
|
||||||
BrowseSettled(SubscriptionId),
|
BrowseSettled(SubscriptionId),
|
||||||
|
ContactListsSettled,
|
||||||
|
ContactPreviewSettled(SubscriptionId),
|
||||||
|
DiscoverySettled,
|
||||||
Forks,
|
Forks,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +68,14 @@ fn fork_subscription_id() -> SubscriptionId {
|
|||||||
SubscriptionId::new(FORK_SUBSCRIPTION)
|
SubscriptionId::new(FORK_SUBSCRIPTION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn contacts_subscription_id() -> SubscriptionId {
|
||||||
|
SubscriptionId::new(CONTACTS_SUBSCRIPTION)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discovery_subscription_id() -> SubscriptionId {
|
||||||
|
SubscriptionId::new(DISCOVERY_SUBSCRIPTION)
|
||||||
|
}
|
||||||
|
|
||||||
fn subscription_id(key: &GroupKey) -> SubscriptionId {
|
fn subscription_id(key: &GroupKey) -> SubscriptionId {
|
||||||
SubscriptionId::new(format!("{GROUP_PREFIX}{:08x}", key.uniq_id() as u32))
|
SubscriptionId::new(format!("{GROUP_PREFIX}{:08x}", key.uniq_id() as u32))
|
||||||
}
|
}
|
||||||
@@ -71,6 +87,13 @@ fn browse_subscription(relay: &RelayUrl) -> SubscriptionId {
|
|||||||
SubscriptionId::new(format!("{BROWSE_PREFIX}{:08x}", hasher.finish() as u32))
|
SubscriptionId::new(format!("{BROWSE_PREFIX}{:08x}", hasher.finish() as u32))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn preview_subscription(relay: &RelayUrl) -> SubscriptionId {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
relay.hash(&mut hasher);
|
||||||
|
|
||||||
|
SubscriptionId::new(format!("{PREVIEW_PREFIX}{:08x}", hasher.finish() as u32))
|
||||||
|
}
|
||||||
|
|
||||||
fn page_subscription(key: &GroupKey) -> SubscriptionId {
|
fn page_subscription(key: &GroupKey) -> SubscriptionId {
|
||||||
SubscriptionId::new(format!("{PAGE_PREFIX}{:08x}", key.uniq_id() as u32))
|
SubscriptionId::new(format!("{PAGE_PREFIX}{:08x}", key.uniq_id() as u32))
|
||||||
}
|
}
|
||||||
@@ -83,8 +106,11 @@ fn is_own_subscription(id: &SubscriptionId) -> bool {
|
|||||||
let id = id.as_str();
|
let id = id.as_str();
|
||||||
id == LIST_SUBSCRIPTION
|
id == LIST_SUBSCRIPTION
|
||||||
|| id == FORK_SUBSCRIPTION
|
|| id == FORK_SUBSCRIPTION
|
||||||
|
|| id == CONTACTS_SUBSCRIPTION
|
||||||
|
|| id == DISCOVERY_SUBSCRIPTION
|
||||||
|| id.starts_with(GROUP_PREFIX)
|
|| id.starts_with(GROUP_PREFIX)
|
||||||
|| id.starts_with(BROWSE_PREFIX)
|
|| id.starts_with(BROWSE_PREFIX)
|
||||||
|
|| id.starts_with(PREVIEW_PREFIX)
|
||||||
|| id.starts_with(PAGE_PREFIX)
|
|| id.starts_with(PAGE_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +124,8 @@ pub struct GroupsRegistry {
|
|||||||
observers: HashMap<GroupKey, Subscription>,
|
observers: HashMap<GroupKey, Subscription>,
|
||||||
forks: HashMap<PublicKey, (Timestamp, GroupList)>,
|
forks: HashMap<PublicKey, (Timestamp, GroupList)>,
|
||||||
fork_authors: HashSet<PublicKey>,
|
fork_authors: HashSet<PublicKey>,
|
||||||
|
/// The groups discovered from the cache and the relays.
|
||||||
|
discovery: Entity<Discovery>,
|
||||||
signal_tx: flume::Sender<Signal>,
|
signal_tx: flume::Sender<Signal>,
|
||||||
signal_rx: flume::Receiver<Signal>,
|
signal_rx: flume::Receiver<Signal>,
|
||||||
notification_listener: Option<Task<()>>,
|
notification_listener: Option<Task<()>>,
|
||||||
@@ -152,6 +180,7 @@ impl GroupsRegistry {
|
|||||||
observers: HashMap::new(),
|
observers: HashMap::new(),
|
||||||
forks: HashMap::new(),
|
forks: HashMap::new(),
|
||||||
fork_authors: HashSet::new(),
|
fork_authors: HashSet::new(),
|
||||||
|
discovery: cx.new(|_| Discovery::default()),
|
||||||
signal_tx: tx,
|
signal_tx: tx,
|
||||||
signal_rx: rx,
|
signal_rx: rx,
|
||||||
notification_listener: None,
|
notification_listener: None,
|
||||||
@@ -280,6 +309,426 @@ impl GroupsRegistry {
|
|||||||
cx.emit(GroupsEvent::BrowseSettled(relay));
|
cx.emit(GroupsEvent::BrowseSettled(relay));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The groups discovered from the cache and the relays.
|
||||||
|
pub fn discovery(&self) -> Entity<Discovery> {
|
||||||
|
self.discovery.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discover(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
|
if nostr.read(cx).current_user().is_none() {
|
||||||
|
// There is nothing to discover without a user.
|
||||||
|
self.discovery.update(cx, |discovery, cx| {
|
||||||
|
discovery.settle_all(cx);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every open sweeps the relays again, starting from the cache.
|
||||||
|
self.discovery.update(cx, |discovery, cx| {
|
||||||
|
discovery.begin(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
type Cached = (HashSet<PublicKey>, Vec<Event>);
|
||||||
|
|
||||||
|
let task: Task<Result<Cached, Error>> = cx.background_spawn(async move {
|
||||||
|
let me = signer.get_public_key_async().await?;
|
||||||
|
let filter = Filter::new().author(me).kind(Kind::ContactList).limit(1);
|
||||||
|
|
||||||
|
let contacts = client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.map(|event| event.tags.public_keys().collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let lists = client
|
||||||
|
.database()
|
||||||
|
.query(Filter::new().kind(Kind::SimpleGroups))
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok((contacts, lists))
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
let (authors, lists) = match task.await {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("nip29: loading the cached groups failed: {error}");
|
||||||
|
(HashSet::new(), Vec::new())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = this.update(cx, |this, cx| {
|
||||||
|
let newest: HashMap<PublicKey, Event> =
|
||||||
|
lists.into_iter().fold(HashMap::new(), |mut newest, event| {
|
||||||
|
match newest.get(&event.pubkey) {
|
||||||
|
Some(current) if current.created_at >= event.created_at => newest,
|
||||||
|
_ => {
|
||||||
|
newest.insert(event.pubkey, event);
|
||||||
|
newest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for event in newest.into_values() {
|
||||||
|
this.absorb_group_list(&event, cx);
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
log::warn!("nip29: applying the cached group lists failed: {error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let previews = match this.update(cx, |this, cx| this.load_cached_previews(cx)) {
|
||||||
|
Ok(task) => match task.await {
|
||||||
|
Ok(previews) => previews,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("nip29: loading the cached group metadata failed: {error}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("nip29: discovering the groups failed: {error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = this.update(cx, |this, cx| {
|
||||||
|
this.absorb_cached_previews(previews, cx);
|
||||||
|
this.subscribe_contacts(authors, cx);
|
||||||
|
this.subscribe_discovery(cx);
|
||||||
|
}) {
|
||||||
|
log::warn!("nip29: discovering the groups failed: {error}");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the cached metadata for the referenced groups from the database.
|
||||||
|
fn load_cached_previews(&self, cx: &mut Context<Self>) -> Task<Result<Vec<Event>, Error>> {
|
||||||
|
let refs = self.discovery.read(cx).refs().clone();
|
||||||
|
|
||||||
|
if refs.is_empty() {
|
||||||
|
return Task::ready(Ok(Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let ids: HashSet<&str> = refs.values().flatten().map(GroupId::as_str).collect();
|
||||||
|
let filter = Filter::new().kind(Kind::GroupMetadata).identifiers(ids);
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
Ok(client.database().query(filter).await?.into_iter().collect())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seed the discoveries from the cached group metadata.
|
||||||
|
fn absorb_cached_previews(&mut self, previews: Vec<Event>, cx: &mut Context<Self>) {
|
||||||
|
let newest: HashMap<GroupId, (Timestamp, GroupMetadata)> =
|
||||||
|
previews
|
||||||
|
.into_iter()
|
||||||
|
.fold(HashMap::new(), |mut newest, event| {
|
||||||
|
let metadata = match GroupMetadata::parse(&event) {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("nip29: ignoring malformed cached group metadata: {error}");
|
||||||
|
return newest;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(id) = metadata.id().cloned() else {
|
||||||
|
return newest;
|
||||||
|
};
|
||||||
|
|
||||||
|
match newest.get(&id) {
|
||||||
|
Some((created_at, _)) if *created_at >= event.created_at => newest,
|
||||||
|
_ => {
|
||||||
|
newest.insert(id, (event.created_at, metadata));
|
||||||
|
newest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
|
for (relay, ids) in self.discovery.read(cx).refs().clone() {
|
||||||
|
for id in ids {
|
||||||
|
let Some((_, metadata)) = newest.get(&id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hidden groups are not discoverable (NIP-29).
|
||||||
|
if metadata.hidden {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.push(GroupCandidate {
|
||||||
|
key: GroupKey::new(relay.clone(), id.clone()),
|
||||||
|
metadata: metadata.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.discovery.update(cx, |discovery, cx| {
|
||||||
|
for candidate in candidates {
|
||||||
|
discovery.upsert(candidate, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to the contacts' public group lists.
|
||||||
|
fn subscribe_contacts(&mut self, authors: HashSet<PublicKey>, cx: &mut Context<Self>) {
|
||||||
|
if authors.is_empty() {
|
||||||
|
// Nobody is followed, so there is nothing to discover from them.
|
||||||
|
self.discovery
|
||||||
|
.update(cx, |discovery, cx| discovery.settle_lists(cx));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let filter = Filter::new().authors(authors).kind(Kind::SimpleGroups);
|
||||||
|
|
||||||
|
let opts = SubscribeAutoCloseOptions::default()
|
||||||
|
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||||
|
.timeout(Some(BROWSE_TIMEOUT));
|
||||||
|
|
||||||
|
let tx = self.signal_tx.clone();
|
||||||
|
|
||||||
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
|
let refused = match client
|
||||||
|
.subscribe(ReqTarget::auto([filter]))
|
||||||
|
.with_id(contacts_subscription_id())
|
||||||
|
.close_on(opts)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(output) => output.success.is_empty(),
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("subscribing to the contacts group lists failed: {error}");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// When every relay refused the request no EOSE will arrive.
|
||||||
|
if refused && let Err(error) = tx.send_async(Signal::ContactListsSettled).await {
|
||||||
|
log::warn!("the contacts settle signal was dropped: {error}");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to the metadata of every group the pool relays host.
|
||||||
|
fn subscribe_discovery(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let filter = Filter::new().kind(Kind::GroupMetadata).limit(BROWSE_LIMIT);
|
||||||
|
|
||||||
|
let opts = SubscribeAutoCloseOptions::default()
|
||||||
|
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||||
|
.timeout(Some(BROWSE_TIMEOUT));
|
||||||
|
|
||||||
|
let tx = self.signal_tx.clone();
|
||||||
|
|
||||||
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
|
let refused = match client
|
||||||
|
.subscribe(ReqTarget::auto([filter]))
|
||||||
|
.with_id(discovery_subscription_id())
|
||||||
|
.close_on(opts)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(output) => output.success.is_empty(),
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("nip29: the group discovery failed: {error}");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// When every relay refused the request no EOSE will arrive.
|
||||||
|
if refused && let Err(error) = tx.send_async(Signal::DiscoverySettled).await {
|
||||||
|
log::warn!("the discovery settle signal was dropped: {error}");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Absorb a discovered group metadata event from the relay serving it.
|
||||||
|
fn absorb_discovered(&mut self, relay: RelayUrl, event: &Event, cx: &mut Context<Self>) {
|
||||||
|
let metadata = match GroupMetadata::parse(event) {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("nip29: ignoring malformed group metadata from {relay}: {error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hidden groups are not discoverable (NIP-29).
|
||||||
|
if metadata.hidden {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(id) = metadata.id().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.discovery.update(cx, |discovery, cx| {
|
||||||
|
discovery.upsert(
|
||||||
|
GroupCandidate {
|
||||||
|
key: GroupKey::new(relay, id),
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask every relay hosting referenced groups for the groups' metadata.
|
||||||
|
fn sync_previews(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
for (relay, ids) in self.discovery.read(cx).refs().clone() {
|
||||||
|
// A subscription is already asking this relay for previews.
|
||||||
|
if self.discovery.read(cx).preview_in_flight(&relay) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = preview_subscription(&relay);
|
||||||
|
self.discovery.update(cx, |discovery, _| {
|
||||||
|
discovery.track_preview(id.clone(), relay.clone());
|
||||||
|
});
|
||||||
|
let client = client.clone();
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::GroupMetadata)
|
||||||
|
.identifiers(ids.iter().map(GroupId::as_str));
|
||||||
|
|
||||||
|
let opts = SubscribeAutoCloseOptions::default()
|
||||||
|
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||||
|
.timeout(Some(BROWSE_TIMEOUT));
|
||||||
|
|
||||||
|
let tx = self.signal_tx.clone();
|
||||||
|
|
||||||
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
|
client.add_relay(relay.clone()).and_connect().await.ok();
|
||||||
|
|
||||||
|
let refused = match client
|
||||||
|
.subscribe(ReqTarget::single(relay.clone(), [filter]))
|
||||||
|
.with_id(id.clone())
|
||||||
|
.close_on(opts)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(output) => output.success.is_empty(),
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("fetching group previews from {relay} failed: {error}");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// When the relay refused the request no EOSE will arrive.
|
||||||
|
if refused
|
||||||
|
&& let Err(error) = tx.send_async(Signal::ContactPreviewSettled(id)).await
|
||||||
|
{
|
||||||
|
log::warn!("the preview settle signal was dropped: {error}");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn absorb_group_list(&mut self, event: &Event, cx: &mut Context<Self>) {
|
||||||
|
let Ok(list) = GroupList::parse(event) else {
|
||||||
|
log::warn!(
|
||||||
|
"nip29: ignoring a malformed group list from {}",
|
||||||
|
event.pubkey
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let joined: HashSet<(RelayUrl, GroupId)> = self
|
||||||
|
.groups
|
||||||
|
.iter()
|
||||||
|
.map(|group| {
|
||||||
|
let key = group.read(cx).key();
|
||||||
|
(key.relay().clone(), key.id().clone())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let added = self.discovery.update(cx, |discovery, cx| {
|
||||||
|
discovery.absorb_list(&list, &joined, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
if added && self.discovery.read(cx).lists_settled() {
|
||||||
|
self.sync_previews(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn absorb_preview(&mut self, id: &SubscriptionId, event: &Event, cx: &mut Context<Self>) {
|
||||||
|
let Some(relay) = self.discovery.read(cx).preview_relay(id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadata = match GroupMetadata::parse(event) {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("nip29: ignoring malformed group metadata from {relay}: {error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hidden groups are not discoverable (NIP-29).
|
||||||
|
if metadata.hidden {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A relay may answer with more than the filter asked for.
|
||||||
|
let Some(group) = metadata.id().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self
|
||||||
|
.discovery
|
||||||
|
.read(cx)
|
||||||
|
.refs()
|
||||||
|
.get(&relay)
|
||||||
|
.is_some_and(|ids| ids.contains(&group))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.discovery.update(cx, |discovery, cx| {
|
||||||
|
discovery.upsert(
|
||||||
|
GroupCandidate {
|
||||||
|
key: GroupKey::new(relay, group),
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contacts' group lists have been answered: resolve the metadata.
|
||||||
|
fn handle_lists_settled(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.discovery
|
||||||
|
.update(cx, |discovery, cx| discovery.settle_lists(cx));
|
||||||
|
self.sync_previews(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_preview_settled(&mut self, id: &SubscriptionId, cx: &mut Context<Self>) {
|
||||||
|
self.discovery.update(cx, |discovery, cx| {
|
||||||
|
discovery.settle_preview(id, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_more(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
|
pub fn load_more(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
|
||||||
let Some(group) = self.group(key, cx) else {
|
let Some(group) = self.group(key, cx) else {
|
||||||
return;
|
return;
|
||||||
@@ -336,9 +785,13 @@ impl GroupsRegistry {
|
|||||||
let mut ids: Vec<SubscriptionId> = routes.into_keys().collect();
|
let mut ids: Vec<SubscriptionId> = routes.into_keys().collect();
|
||||||
ids.push(list_subscription_id());
|
ids.push(list_subscription_id());
|
||||||
ids.push(fork_subscription_id());
|
ids.push(fork_subscription_id());
|
||||||
|
ids.push(discovery_subscription_id());
|
||||||
ids.extend(self.browse.keys().cloned());
|
ids.extend(self.browse.keys().cloned());
|
||||||
|
ids.extend(self.discovery.read(cx).preview_ids());
|
||||||
|
|
||||||
self.browse.clear();
|
self.browse.clear();
|
||||||
|
self.discovery
|
||||||
|
.update(cx, |discovery, cx| discovery.reset(cx));
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
for id in ids {
|
for id in ids {
|
||||||
@@ -803,7 +1256,13 @@ impl GroupsRegistry {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_event(&mut self, id: SubscriptionId, event: Event, cx: &mut Context<Self>) {
|
fn handle_event(
|
||||||
|
&mut self,
|
||||||
|
id: SubscriptionId,
|
||||||
|
relay: RelayUrl,
|
||||||
|
event: Event,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
if id == list_subscription_id() {
|
if id == list_subscription_id() {
|
||||||
match GroupList::parse(&event) {
|
match GroupList::parse(&event) {
|
||||||
Ok(list) => self.merge(list, cx),
|
Ok(list) => self.merge(list, cx),
|
||||||
@@ -817,11 +1276,26 @@ impl GroupsRegistry {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if id == contacts_subscription_id() {
|
||||||
|
self.absorb_group_list(&event, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == discovery_subscription_id() {
|
||||||
|
self.absorb_discovered(relay, &event, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if self.browse.contains_key(&id) {
|
if self.browse.contains_key(&id) {
|
||||||
self.absorb_candidate(&id, &event, cx);
|
self.absorb_candidate(&id, &event, cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.discovery.read(cx).preview_relay(&id).is_some() {
|
||||||
|
self.absorb_preview(&id, &event, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(key) = self.routes.get(&id).cloned() else {
|
let Some(key) = self.routes.get(&id).cloned() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -858,6 +1332,22 @@ impl GroupsRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_eose(&mut self, id: SubscriptionId, cx: &mut Context<Self>) {
|
fn handle_eose(&mut self, id: SubscriptionId, cx: &mut Context<Self>) {
|
||||||
|
if id == contacts_subscription_id() {
|
||||||
|
self.handle_lists_settled(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == discovery_subscription_id() {
|
||||||
|
self.discovery
|
||||||
|
.update(cx, |discovery, cx| discovery.settle_sweep(cx));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.discovery.read(cx).preview_relay(&id).is_some() {
|
||||||
|
self.handle_preview_settled(&id, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(key) = self.routes.get(&id).cloned() else {
|
let Some(key) = self.routes.get(&id).cloned() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -886,6 +1376,25 @@ impl GroupsRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_closed(&mut self, id: SubscriptionId, reason: String, cx: &mut Context<Self>) {
|
fn handle_closed(&mut self, id: SubscriptionId, reason: String, cx: &mut Context<Self>) {
|
||||||
|
if id == contacts_subscription_id() {
|
||||||
|
log::warn!("nip29: the contacts' group lists were refused: {reason}");
|
||||||
|
self.handle_lists_settled(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == discovery_subscription_id() {
|
||||||
|
log::warn!("nip29: the group discovery was closed: {reason}");
|
||||||
|
self.discovery
|
||||||
|
.update(cx, |discovery, cx| discovery.settle_sweep(cx));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(relay) = self.discovery.read(cx).preview_relay(&id) {
|
||||||
|
log::warn!("nip29: fetching previews from {relay} was refused: {reason}");
|
||||||
|
self.handle_preview_settled(&id, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(relay) = self.browse.get(&id) {
|
if let Some(relay) = self.browse.get(&id) {
|
||||||
log::warn!("nip29: browsing {relay} was refused: {reason}");
|
log::warn!("nip29: browsing {relay} was refused: {reason}");
|
||||||
return;
|
return;
|
||||||
@@ -943,7 +1452,7 @@ impl GroupsRegistry {
|
|||||||
const MAX_PROCESSED: usize = 10_000;
|
const MAX_PROCESSED: usize = 10_000;
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
while let Some(notification) = notifications.next().await {
|
||||||
let ClientNotification::Message { message, .. } = notification else {
|
let ClientNotification::Message { relay_url, message } = notification else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -962,7 +1471,7 @@ impl GroupsRegistry {
|
|||||||
}
|
}
|
||||||
processed
|
processed
|
||||||
.insert(event.id)
|
.insert(event.id)
|
||||||
.then(|| Signal::Event(id, event.into_owned()))
|
.then(|| Signal::Event(id, relay_url, Box::new(event.into_owned())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RelayMessage::EndOfStoredEvents(id) => {
|
RelayMessage::EndOfStoredEvents(id) => {
|
||||||
@@ -988,8 +1497,8 @@ impl GroupsRegistry {
|
|||||||
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
|
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
|
||||||
while let Ok(signal) = rx.recv_async().await {
|
while let Ok(signal) = rx.recv_async().await {
|
||||||
this.update(cx, |this, cx| match signal {
|
this.update(cx, |this, cx| match signal {
|
||||||
Signal::Event(id, event) => {
|
Signal::Event(id, relay, event) => {
|
||||||
this.handle_event(id, event, cx);
|
this.handle_event(id, relay, *event, cx);
|
||||||
}
|
}
|
||||||
Signal::Eose(id) => {
|
Signal::Eose(id) => {
|
||||||
this.handle_eose(id, cx);
|
this.handle_eose(id, cx);
|
||||||
@@ -1000,6 +1509,16 @@ impl GroupsRegistry {
|
|||||||
Signal::BrowseSettled(id) => {
|
Signal::BrowseSettled(id) => {
|
||||||
this.settle_browse(id, cx);
|
this.settle_browse(id, cx);
|
||||||
}
|
}
|
||||||
|
Signal::ContactListsSettled => {
|
||||||
|
this.handle_lists_settled(cx);
|
||||||
|
}
|
||||||
|
Signal::ContactPreviewSettled(id) => {
|
||||||
|
this.handle_preview_settled(&id, cx);
|
||||||
|
}
|
||||||
|
Signal::DiscoverySettled => {
|
||||||
|
this.discovery
|
||||||
|
.update(cx, |discovery, cx| discovery.settle_sweep(cx));
|
||||||
|
}
|
||||||
Signal::Forks => {
|
Signal::Forks => {
|
||||||
this.sync_forks(true, cx);
|
this.sync_forks(true, cx);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-18
@@ -30,9 +30,6 @@ pub use nip05::*;
|
|||||||
pub use signer::{CoopAuthUrlHandler, UniversalSigner};
|
pub use signer::{CoopAuthUrlHandler, UniversalSigner};
|
||||||
|
|
||||||
pub fn init(cx: &mut App, cli_key: Option<SecretKey>) {
|
pub fn init(cx: &mut App, cli_key: Option<SecretKey>) {
|
||||||
// rustls uses the `aws_lc_rs` provider by default
|
|
||||||
// This only errors if the default provider has already
|
|
||||||
// been installed. We can ignore this `Result`.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
rustls::crypto::aws_lc_rs::default_provider()
|
rustls::crypto::aws_lc_rs::default_provider()
|
||||||
.install_default()
|
.install_default()
|
||||||
@@ -136,22 +133,20 @@ impl NostrRegistry {
|
|||||||
|
|
||||||
// Connect to bootstrap relays once the registry has been returned to the app
|
// Connect to bootstrap relays once the registry has been returned to the app
|
||||||
cx.defer(move |cx| {
|
cx.defer(move |cx| {
|
||||||
entity
|
let _ = entity.update(cx, |this, cx| {
|
||||||
.update(cx, |this, cx| {
|
this.connect_bootstrap_relays(cx);
|
||||||
this.connect_bootstrap_relays(cx);
|
|
||||||
|
|
||||||
if cfg!(target_arch = "wasm32") {
|
if cfg!(target_arch = "wasm32") {
|
||||||
this.mark_ready(cx);
|
this.mark_ready(cx);
|
||||||
cx.emit(StateEvent::NoSigner);
|
cx.emit(StateEvent::NoSigner);
|
||||||
} else if let Some(secret) = cli_key {
|
} else if let Some(secret) = cli_key {
|
||||||
// Use CLI-provided key -- same path as get_user_credential
|
// Use CLI-provided key -- same path as get_user_credential
|
||||||
let keys = Keys::new(secret);
|
let keys = Keys::new(secret);
|
||||||
this.set_signer(keys, cx);
|
this.set_signer(keys, cx);
|
||||||
} else {
|
} else {
|
||||||
this.get_user_credential(cx);
|
this.get_user_credential(cx);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.ok();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use gpui::{
|
|||||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
||||||
div, uniform_list,
|
div, uniform_list,
|
||||||
};
|
};
|
||||||
use nip29::{GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry};
|
use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry};
|
||||||
use nostr_sdk::prelude::RelayUrl;
|
use nostr_sdk::prelude::RelayUrl;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -35,15 +35,18 @@ pub struct BrowsePanel {
|
|||||||
relay: Option<RelayUrl>,
|
relay: Option<RelayUrl>,
|
||||||
/// The groups the relay has announced so far
|
/// The groups the relay has announced so far
|
||||||
candidates: Vec<GroupCandidate>,
|
candidates: Vec<GroupCandidate>,
|
||||||
|
/// The groups discovered from the cache and the relays
|
||||||
|
discovery: Entity<Discovery>,
|
||||||
/// Whether the relay is still answering
|
/// Whether the relay is still answering
|
||||||
loading: bool,
|
loading: bool,
|
||||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowsePanel {
|
impl BrowsePanel {
|
||||||
fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder(INPUT_PLACEHOLDER));
|
let input = cx.new(|cx| InputState::new(window, cx).placeholder(INPUT_PLACEHOLDER));
|
||||||
let registry = GroupsRegistry::global(cx);
|
let registry = GroupsRegistry::global(cx);
|
||||||
|
let discovery = registry.read(cx).discovery();
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
@@ -55,6 +58,10 @@ impl BrowsePanel {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
subscriptions.push(cx.observe(&discovery, |_, _, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
cx.subscribe(®istry, |this, _registry, event, cx| match event {
|
cx.subscribe(®istry, |this, _registry, event, cx| match event {
|
||||||
GroupsEvent::Browsed(candidate) => {
|
GroupsEvent::Browsed(candidate) => {
|
||||||
@@ -67,12 +74,20 @@ impl BrowsePanel {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Wait for the panel to leave the stack before kicking off the sweep.
|
||||||
|
cx.defer_in(window, |_this, _window, cx| {
|
||||||
|
GroupsRegistry::global(cx).update(cx, |registry, cx| {
|
||||||
|
registry.discover(cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
dock,
|
dock,
|
||||||
input,
|
input,
|
||||||
relay: None,
|
relay: None,
|
||||||
candidates: Vec::new(),
|
candidates: Vec::new(),
|
||||||
|
discovery,
|
||||||
loading: false,
|
loading: false,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
@@ -150,11 +165,17 @@ impl BrowsePanel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_rows(&self, range: Range<usize>, cx: &mut Context<Self>) -> Vec<AnyElement> {
|
fn render_rows(
|
||||||
|
&self,
|
||||||
|
candidates: &[GroupCandidate],
|
||||||
|
relay_detail: bool,
|
||||||
|
range: Range<usize>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Vec<AnyElement> {
|
||||||
|
let registry = GroupsRegistry::global(cx);
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
let panel = cx.entity().downgrade();
|
let panel = cx.entity().downgrade();
|
||||||
|
|
||||||
let registry = GroupsRegistry::global(cx);
|
|
||||||
let joined: Vec<GroupKey> = registry
|
let joined: Vec<GroupKey> = registry
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.groups()
|
.groups()
|
||||||
@@ -162,18 +183,26 @@ impl BrowsePanel {
|
|||||||
.map(|group| group.read(cx).key().clone())
|
.map(|group| group.read(cx).key().clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.candidates
|
candidates
|
||||||
.get(range)
|
.get(range)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|candidate| {
|
.map(|candidate| {
|
||||||
let key = candidate.key.clone();
|
let key = candidate.key.clone();
|
||||||
let name = SharedString::from(candidate.display_name().to_owned());
|
let name = SharedString::from(candidate.display_name().to_owned());
|
||||||
|
|
||||||
|
let seed = key.cache_tag();
|
||||||
|
let is_joined = joined.contains(&key);
|
||||||
|
|
||||||
let picture = candidate
|
let picture = candidate
|
||||||
.display_image()
|
.display_image()
|
||||||
.map(|picture| SharedString::from(picture.to_owned()));
|
.map(|picture| SharedString::from(picture.to_owned()));
|
||||||
let seed = key.cache_tag();
|
|
||||||
let is_joined = joined.contains(&key);
|
let detail = if relay_detail {
|
||||||
|
key.relay().domain().unwrap_or_else(|| key.relay().as_str())
|
||||||
|
} else {
|
||||||
|
key.id().as_str()
|
||||||
|
};
|
||||||
|
|
||||||
let action = if is_joined {
|
let action = if is_joined {
|
||||||
Button::new(format!("browse-joined-{seed}"))
|
Button::new(format!("browse-joined-{seed}"))
|
||||||
@@ -221,7 +250,7 @@ impl BrowsePanel {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_placeholder)
|
.text_color(cx.theme().text_placeholder)
|
||||||
.truncate()
|
.truncate()
|
||||||
.child(SharedString::from(key.id().as_str().to_owned())),
|
.child(SharedString::from(detail.to_owned())),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(action)
|
.child(action)
|
||||||
@@ -270,14 +299,56 @@ impl Focusable for BrowsePanel {
|
|||||||
impl Render for BrowsePanel {
|
impl Render for BrowsePanel {
|
||||||
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 count = self.candidates.len();
|
let count = self.candidates.len();
|
||||||
|
let discover = self.relay.is_none();
|
||||||
|
|
||||||
let body = if self.relay.is_none() {
|
let (discovered, discovered_count, discovering) = if discover {
|
||||||
notice("Enter a relay to browse its groups.", cx)
|
let registry = GroupsRegistry::global(cx);
|
||||||
|
let joined: Vec<GroupKey> = registry
|
||||||
|
.read(cx)
|
||||||
|
.groups()
|
||||||
|
.iter()
|
||||||
|
.map(|group| group.read(cx).key().clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let discovery = self.discovery.read(cx);
|
||||||
|
let discovering = discovery.loading();
|
||||||
|
|
||||||
|
let discovered: Vec<GroupCandidate> = discovery
|
||||||
|
.discovered()
|
||||||
|
.iter()
|
||||||
|
.filter(|candidate| !joined.contains(&candidate.key))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let count = discovered.len();
|
||||||
|
(discovered, count, discovering)
|
||||||
|
} else {
|
||||||
|
(Vec::new(), 0, false)
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = if discover {
|
||||||
|
if discovered_count > 0 {
|
||||||
|
uniform_list(
|
||||||
|
"browse-discovered",
|
||||||
|
discovered_count,
|
||||||
|
cx.processor(move |this, range, _window, cx| {
|
||||||
|
this.render_rows(&discovered, true, range, cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.h_full()
|
||||||
|
.into_any_element()
|
||||||
|
} else if discovering {
|
||||||
|
notice("Discovering groups…", cx)
|
||||||
|
} else {
|
||||||
|
notice("No groups found.", cx)
|
||||||
|
}
|
||||||
} else if count > 0 {
|
} else if count > 0 {
|
||||||
uniform_list(
|
uniform_list(
|
||||||
"browse-groups",
|
"browse-groups",
|
||||||
count,
|
count,
|
||||||
cx.processor(|this, range, _window, cx| this.render_rows(range, cx)),
|
cx.processor(|this, range, _window, cx| {
|
||||||
|
this.render_rows(&this.candidates, false, range, cx)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.h_full()
|
.h_full()
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -303,10 +374,20 @@ impl Render for BrowsePanel {
|
|||||||
this.browse(window, cx);
|
this.browse(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.when(self.loading, |this| {
|
.when(self.loading || (discover && discovering), |this| {
|
||||||
this.child(Indicator::new().small().color(cx.theme().icon_accent))
|
this.child(Indicator::new().small().color(cx.theme().icon_accent))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.when(discover && discovered_count > 0, |this| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.px_3()
|
||||||
|
.pb_1()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("Discovered groups"),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(div().flex_1().min_h_0().child(body))
|
.child(div().flex_1().min_h_0().child(body))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user