update community

This commit is contained in:
2026-09-19 10:33:11 +07:00
parent 97e7539cea
commit cbb97e471f
8 changed files with 190 additions and 21 deletions
+54 -4
View File
@@ -1,7 +1,8 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use anyhow::Result;
use concord::cord02::ControlFold;
use concord::cord02::{ControlFold, ImageRef};
use concord::store::{ChannelKeyRef, CommunityState};
use concord::{ChannelId, CommunityId, Epoch};
use gpui::{AppContext, Context, EventEmitter, Task};
@@ -45,8 +46,11 @@ pub struct Community {
state: CommunityState,
control: ControlFold,
members: BTreeSet<PublicKey>,
icon: Option<PathBuf>,
icon_ref: Option<ImageRef>,
dirty: bool,
refresh_task: Option<Task<Result<()>>>,
icon_task: Option<Task<Result<()>>>,
}
impl EventEmitter<CommunityEvent> for Community {}
@@ -57,8 +61,11 @@ impl Community {
state,
control: ControlFold::default(),
members: BTreeSet::new(),
icon: None,
icon_ref: None,
dirty: false,
refresh_task: None,
icon_task: None,
}
}
@@ -71,16 +78,25 @@ impl Community {
}
pub fn name(&self) -> String {
match &self.control.community {
Some(metadata) => metadata.name.clone(),
None => self.state.id.to_hex(),
if let Some(metadata) = &self.control.community {
return metadata.name.clone();
}
self.state
.name
.clone()
.unwrap_or_else(|| self.state.id.to_hex())
}
pub fn control(&self) -> &ControlFold {
&self.control
}
/// The community's icon, once downloaded and decrypted into a cache file.
pub fn icon(&self) -> Option<PathBuf> {
self.icon.clone()
}
pub fn members(&self) -> &BTreeSet<PublicKey> {
&self.members
}
@@ -121,6 +137,7 @@ impl Community {
self.state = snapshot.state;
self.control = snapshot.control;
self.members = snapshot.members;
self.load_icon(cx);
cx.emit(CommunityEvent::Updated(self.state.id));
cx.notify();
}
@@ -133,4 +150,37 @@ impl Community {
self.refresh(cx);
}
}
/// Resolve the folded icon into a local file.
fn load_icon(&mut self, cx: &mut Context<Self>) {
let icon = self
.control
.community
.as_ref()
.and_then(|metadata| metadata.icon.clone());
if self.icon_ref == icon {
return;
}
self.icon_ref = icon.clone();
self.icon = None;
let Some(icon) = icon else {
return;
};
self.icon_task = Some(cx.spawn(async move |this, cx| {
match sync::resolve_icon(&icon, cx).await {
Ok(path) => {
this.update(cx, |this, cx| {
this.icon = Some(path);
cx.notify();
})?;
}
Err(error) => log::warn!("community icon: {error}"),
}
Ok(())
}));
}
}
-1
View File
@@ -357,7 +357,6 @@ async fn subscribe(
relays: &[RelayUrl],
filter: Filter,
) -> Result<()> {
log::info!("community {id}: subscribing to {relays:?}");
client.unsubscribe(id).await?;
for url in relays {
+25 -5
View File
@@ -1,9 +1,10 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use anyhow::Result;
use anyhow::{Context, Result};
use concord::cord01::KIND_WRAP;
use concord::cord02::list::{CommunityList, KIND_COMMUNITY_LIST};
use concord::cord02::{self, ControlFold};
use concord::cord02::{self, ControlFold, ImageRef};
use concord::cord04::AuthorityCitation;
use concord::cord04::roles::{Permissions, citation_ok};
use concord::derive::{
@@ -11,6 +12,7 @@ use concord::derive::{
};
use concord::store::{self, CommunityState};
use concord::{ChannelId, CommunityId, Epoch, GroupKey};
use gpui::AsyncApp;
use nostr_sdk::prelude::*;
use state::UniversalSigner;
@@ -42,6 +44,15 @@ pub fn planes(state: &CommunityState) -> Result<Vec<Plane>> {
});
}
if state.control_pks.is_empty() {
let group = control_group_key(&state.community_root, &state.id, state.root_epoch)?;
planes.push(Plane {
kind: PlaneKind::Control(state.root_epoch),
address: group.pk(),
group,
});
}
let group = guestbook_group_key(&state.community_root, &state.id, state.root_epoch)?;
planes.push(Plane {
kind: PlaneKind::Guestbook,
@@ -85,6 +96,12 @@ pub fn community_of(subscription_id: &SubscriptionId) -> Option<CommunityId> {
.ok()
}
/// Download and decrypt a community icon into a content-addressed cache file.
pub async fn resolve_icon(icon: &ImageRef, cx: &AsyncApp) -> Result<PathBuf> {
let url = Url::parse(&icon.url).context("community icon url")?;
state::download_and_decrypt_to_cache(&url, &icon.key, &icon.nonce, &icon.hash, cx).await
}
#[derive(Debug, Clone)]
pub struct Snapshot {
pub state: CommunityState,
@@ -317,6 +334,10 @@ fn refresh(mut held: CommunityState, fresh: CommunityState) -> CommunityState {
held.control_root = fresh.control_root;
}
if let Some(name) = fresh.name {
held.name = Some(name);
}
for (epoch, address) in fresh.control_pks {
held.control_pks.insert(epoch, address);
}
@@ -517,6 +538,7 @@ mod tests {
let state = CommunityState {
id: CommunityId::from_bytes([0x42; 32]),
name: Some("Anime and Manga".to_owned()),
owner,
owner_salt: [0x01; 32],
community_root: [0x02; 32],
@@ -548,9 +570,6 @@ mod tests {
let planes = planes(&state).expect("planes");
// Control at the root epoch, the guestbook, and the public channel. The
// private channel is skipped: its address derives from the granted key,
// not the community_root.
assert_eq!(planes.len(), 3);
assert!(planes.iter().any(|plane| plane.address == control_pk));
assert!(
@@ -580,6 +599,7 @@ mod tests {
fn held(id: CommunityId, control_pk: PublicKey) -> CommunityState {
CommunityState {
id,
name: Some("Anime and Manga".to_owned()),
owner: Keys::generate().public_key(),
owner_salt: [0x01; 32],
community_root: [0x02; 32],