load community list
This commit is contained in:
@@ -27,6 +27,7 @@ impl Global for GlobalCommunityRegistry {}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum Signal {
|
||||
Event(CommunityId),
|
||||
List,
|
||||
}
|
||||
|
||||
impl EventEmitter<CommunityEvent> for CommunityRegistry {}
|
||||
@@ -67,6 +68,7 @@ impl CommunityRegistry {
|
||||
if event.signer_changed() {
|
||||
this.reset(cx);
|
||||
this.handle_notifications(cx);
|
||||
this.subscribe_list(cx);
|
||||
this.load(cx);
|
||||
}
|
||||
}));
|
||||
@@ -76,6 +78,7 @@ impl CommunityRegistry {
|
||||
.update(cx, |this, cx| {
|
||||
this.handle_notifications(cx);
|
||||
if nostr.read(cx).current_user().is_some() {
|
||||
this.subscribe_list(cx);
|
||||
this.load(cx);
|
||||
}
|
||||
})
|
||||
@@ -163,6 +166,25 @@ impl CommunityRegistry {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Subscribe to the account's community list.
|
||||
fn subscribe_list(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let self_pk = signer.get_public_key_async().await?;
|
||||
|
||||
if let Err(error) = sync::subscribe_list(&client, self_pk).await {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(CommunityEvent::Error(error.to_string()));
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Discover the account's communities in the local database.
|
||||
fn load(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
@@ -298,6 +320,11 @@ impl CommunityRegistry {
|
||||
continue;
|
||||
};
|
||||
|
||||
if sync::is_list_subscription(&subscription_id) {
|
||||
tx.send_async(Signal::List).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if event.kind != Kind::from(KIND_WRAP) {
|
||||
continue;
|
||||
}
|
||||
@@ -313,10 +340,12 @@ impl CommunityRegistry {
|
||||
}));
|
||||
|
||||
self.signal_consumer = Some(cx.spawn(async move |this, cx| {
|
||||
while let Ok(Signal::Event(id)) = rx.recv_async().await {
|
||||
this.update(cx, |this, cx| this.refresh(id, cx))?;
|
||||
while let Ok(signal) = rx.recv_async().await {
|
||||
match signal {
|
||||
Signal::Event(id) => this.update(cx, |this, cx| this.refresh(id, cx))?,
|
||||
Signal::List => this.update(cx, |this, cx| this.load(cx))?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
+151
-11
@@ -121,21 +121,135 @@ where
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Discovers the current account's communities from the local database.
|
||||
/// The subscription id carrying the account's own Community List.
|
||||
pub const LIST_SUBSCRIPTION: &str = "concord/list";
|
||||
|
||||
pub fn list_subscription_id() -> SubscriptionId {
|
||||
SubscriptionId::new(LIST_SUBSCRIPTION)
|
||||
}
|
||||
|
||||
pub fn is_list_subscription(id: &SubscriptionId) -> bool {
|
||||
id.as_str() == LIST_SUBSCRIPTION
|
||||
}
|
||||
|
||||
/// Subscribes to the account's community list.
|
||||
pub async fn subscribe_list(client: &Client, self_pk: PublicKey) -> Result<()> {
|
||||
let id = list_subscription_id();
|
||||
client.unsubscribe(&id).await?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(KIND_COMMUNITY_LIST))
|
||||
.author(self_pk);
|
||||
|
||||
let output = client
|
||||
.subscribe(ReqTarget::auto(vec![filter]))
|
||||
.with_id(id)
|
||||
.await?;
|
||||
|
||||
if !output.failed.is_empty() {
|
||||
log::warn!(
|
||||
"community list: {} relay(s) rejected the subscription",
|
||||
output.failed.len()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discovers the current account's communities: every live membership the List
|
||||
/// carries, plus any locally-held membership the List does not mention.
|
||||
///
|
||||
/// A held membership is dropped only when the List carries a tombstone at least
|
||||
/// as new as it, because absence from the List is never a fact (§8).
|
||||
pub async fn load(
|
||||
client: &Client,
|
||||
signer: &UniversalSigner,
|
||||
self_pk: PublicKey,
|
||||
) -> Result<Vec<CommunityState>> {
|
||||
let mut states = store::load_states(client).await?;
|
||||
let list = match load_list(client, signer, self_pk).await? {
|
||||
Some(list) => list,
|
||||
None => return store::load_states(client).await,
|
||||
};
|
||||
|
||||
if let Some(list) = load_list(client, signer, self_pk).await? {
|
||||
states.retain(|state| list.is_live(&state.id));
|
||||
let mut held: BTreeMap<CommunityId, CommunityState> = store::load_states(client)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|state| (state.id, state))
|
||||
.collect();
|
||||
|
||||
held.retain(|id, state| !retired(&list, id, state.added_at_ms));
|
||||
|
||||
for entry in &list.entries {
|
||||
if !list.is_live(&entry.community_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fresh = match CommunityState::from_join_material(&entry.current, entry.added_at) {
|
||||
Ok(fresh) => fresh,
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"ignoring unreadable community {} from the list: {error}",
|
||||
entry.community_id.to_hex()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let state = match held.remove(&entry.community_id) {
|
||||
Some(materialized) => refresh(materialized, fresh),
|
||||
None => fresh,
|
||||
};
|
||||
|
||||
store::save_state(client, &state).await?;
|
||||
held.insert(entry.community_id, state);
|
||||
}
|
||||
|
||||
Ok(states)
|
||||
Ok(held.into_values().collect())
|
||||
}
|
||||
|
||||
fn retired(list: &CommunityList, id: &CommunityId, added_at_ms: u64) -> bool {
|
||||
list.tombstones
|
||||
.iter()
|
||||
.find(|tombstone| tombstone.community_id == *id)
|
||||
.is_some_and(|tombstone| tombstone.removed_at >= added_at_ms)
|
||||
}
|
||||
|
||||
fn refresh(mut held: CommunityState, fresh: CommunityState) -> CommunityState {
|
||||
held.owner = fresh.owner;
|
||||
held.owner_salt = fresh.owner_salt;
|
||||
held.community_root = fresh.community_root;
|
||||
held.root_epoch = fresh.root_epoch;
|
||||
held.added_at_ms = fresh.added_at_ms;
|
||||
|
||||
if fresh.control_root.is_some() {
|
||||
held.control_root = fresh.control_root;
|
||||
}
|
||||
|
||||
for (epoch, address) in fresh.control_pks {
|
||||
held.control_pks.insert(epoch, address);
|
||||
}
|
||||
|
||||
held.relays = fresh.relays;
|
||||
|
||||
for channel in fresh.channels {
|
||||
match held.channels.iter_mut().find(|held| held.id == channel.id) {
|
||||
Some(held) => {
|
||||
held.name = channel.name;
|
||||
held.epoch = channel.epoch;
|
||||
|
||||
if channel.private {
|
||||
held.private = true;
|
||||
held.key = channel.key;
|
||||
}
|
||||
}
|
||||
None => held.channels.push(channel),
|
||||
}
|
||||
}
|
||||
|
||||
held
|
||||
}
|
||||
|
||||
/// Every fragment of the account's list in the local database, merged.
|
||||
async fn load_list(
|
||||
client: &Client,
|
||||
signer: &UniversalSigner,
|
||||
@@ -143,14 +257,40 @@ async fn load_list(
|
||||
) -> Result<Option<CommunityList>> {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(KIND_COMMUNITY_LIST))
|
||||
.author(self_pk)
|
||||
.limit(1);
|
||||
.author(self_pk);
|
||||
|
||||
let Some(event) = client.database().query(filter).await?.into_iter().next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut newest: BTreeMap<u64, Event> = BTreeMap::new();
|
||||
|
||||
Ok(Some(cord02::list::parse_list_event(signer, &event).await?))
|
||||
for event in client.database().query(filter).await? {
|
||||
let Ok(index) = cord02::list::fragment_index(&event) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match newest.get(&index) {
|
||||
Some(existing) if existing.created_at >= event.created_at => {}
|
||||
_ => {
|
||||
newest.insert(index, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut merged: Option<CommunityList> = None;
|
||||
|
||||
for event in newest.into_values() {
|
||||
match cord02::list::parse_list_event(signer, &event).await {
|
||||
Ok(list) => {
|
||||
merged = Some(match merged {
|
||||
Some(held) => cord02::list::merge(held, list),
|
||||
None => list,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!("ignoring unreadable community list {}: {error}", event.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(merged)
|
||||
}
|
||||
|
||||
/// Rebuilds a community from the wraps already in the local database.
|
||||
|
||||
Reference in New Issue
Block a user