update community sidebar
This commit is contained in:
@@ -45,6 +45,8 @@ impl SubscriptionKey {
|
||||
pub enum CommunityEvent {
|
||||
Updated(CommunityId),
|
||||
Open(CommunityId),
|
||||
Close(CommunityId),
|
||||
Channel(CommunityId, ChannelId),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
@@ -52,11 +54,16 @@ pub struct Community {
|
||||
state: CommunityState,
|
||||
control: ControlFold,
|
||||
members: BTreeSet<PublicKey>,
|
||||
/// The channel the sidebar and panel show, once the user has picked one
|
||||
active: Option<ChannelId>,
|
||||
icon: Option<PathBuf>,
|
||||
icon_ref: Option<ImageRef>,
|
||||
banner: Option<PathBuf>,
|
||||
banner_ref: Option<ImageRef>,
|
||||
dirty: bool,
|
||||
refresh_task: Option<Task<Result<()>>>,
|
||||
icon_task: Option<Task<Result<()>>>,
|
||||
banner_task: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<CommunityEvent> for Community {}
|
||||
@@ -67,11 +74,15 @@ impl Community {
|
||||
state,
|
||||
control: ControlFold::default(),
|
||||
members: BTreeSet::new(),
|
||||
active: None,
|
||||
icon: None,
|
||||
icon_ref: None,
|
||||
banner: None,
|
||||
banner_ref: None,
|
||||
dirty: false,
|
||||
refresh_task: None,
|
||||
icon_task: None,
|
||||
banner_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +114,27 @@ impl Community {
|
||||
self.icon.clone()
|
||||
}
|
||||
|
||||
/// The community's banner, once downloaded and decrypted into a cache file.
|
||||
pub fn banner(&self) -> Option<PathBuf> {
|
||||
self.banner.clone()
|
||||
}
|
||||
|
||||
/// The channel the sidebar and panel show, defaulting to the first one.
|
||||
pub fn active_channel(&self) -> Option<ChannelId> {
|
||||
self.active
|
||||
.or_else(|| self.state.channels.first().map(|channel| channel.id))
|
||||
}
|
||||
|
||||
/// Mark `channel` as the one the sidebar and panel show.
|
||||
pub fn set_active_channel(&mut self, channel: ChannelId, cx: &mut Context<Self>) {
|
||||
if self.active == Some(channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.active = Some(channel);
|
||||
cx.emit(CommunityEvent::Channel(self.state.id, channel));
|
||||
}
|
||||
|
||||
pub fn members(&self) -> &BTreeSet<PublicKey> {
|
||||
&self.members
|
||||
}
|
||||
@@ -241,6 +273,16 @@ impl Community {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Adopt plane material the account's list now carries.
|
||||
///
|
||||
/// The caller re-folds afterwards; this only seeds the new planes.
|
||||
pub(crate) fn adopt(&mut self, state: CommunityState) {
|
||||
// A fold already in flight would write its pre-adoption state back.
|
||||
self.refresh_task = None;
|
||||
self.dirty = false;
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
/// Rebuilds the community from the wraps in the local database.
|
||||
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||
if self.refresh_task.is_some() {
|
||||
@@ -269,7 +311,7 @@ impl Community {
|
||||
self.state = snapshot.state;
|
||||
self.control = snapshot.control;
|
||||
self.members = snapshot.members;
|
||||
self.load_icon(cx);
|
||||
self.load_images(cx);
|
||||
cx.emit(CommunityEvent::Updated(self.state.id));
|
||||
cx.notify();
|
||||
}
|
||||
@@ -283,14 +325,18 @@ impl Community {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
/// Resolve the folded icon and banner into local files.
|
||||
fn load_images(&mut self, cx: &mut Context<Self>) {
|
||||
let (icon, banner) = match self.control.community.as_ref() {
|
||||
Some(metadata) => (metadata.icon.clone(), metadata.banner.clone()),
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
self.load_icon(icon, cx);
|
||||
self.load_banner(banner, cx);
|
||||
}
|
||||
|
||||
fn load_icon(&mut self, icon: Option<ImageRef>, cx: &mut Context<Self>) {
|
||||
if self.icon_ref == icon {
|
||||
return;
|
||||
}
|
||||
@@ -303,7 +349,7 @@ impl Community {
|
||||
};
|
||||
|
||||
self.icon_task = Some(cx.spawn(async move |this, cx| {
|
||||
match sync::resolve_icon(&icon, cx).await {
|
||||
match sync::resolve_image(&icon, cx).await {
|
||||
Ok(path) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.icon = Some(path);
|
||||
@@ -315,4 +361,30 @@ impl Community {
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn load_banner(&mut self, banner: Option<ImageRef>, cx: &mut Context<Self>) {
|
||||
if self.banner_ref == banner {
|
||||
return;
|
||||
}
|
||||
|
||||
self.banner_ref = banner.clone();
|
||||
self.banner = None;
|
||||
|
||||
let Some(banner) = banner else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.banner_task = Some(cx.spawn(async move |this, cx| {
|
||||
match sync::resolve_image(&banner, cx).await {
|
||||
Ok(path) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.banner = Some(path);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
Err(error) => log::warn!("community banner: {error}"),
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
+51
-14
@@ -39,7 +39,7 @@ pub struct CommunityRegistry {
|
||||
/// The plane set each community was last subscribed with
|
||||
synced: HashMap<CommunityId, SubscriptionKey>,
|
||||
/// One observer per tracked community, dropped on reset
|
||||
observers: Vec<Subscription>,
|
||||
observers: HashMap<CommunityId, Subscription>,
|
||||
signal_tx: flume::Sender<Signal>,
|
||||
signal_rx: flume::Receiver<Signal>,
|
||||
tasks: SmallVec<[Task<Result<()>>; 2]>,
|
||||
@@ -90,7 +90,7 @@ impl CommunityRegistry {
|
||||
communities: Vec::new(),
|
||||
index: HashMap::new(),
|
||||
synced: HashMap::new(),
|
||||
observers: Vec::new(),
|
||||
observers: HashMap::new(),
|
||||
signal_tx: tx,
|
||||
signal_rx: rx,
|
||||
tasks: smallvec![],
|
||||
@@ -122,6 +122,13 @@ impl CommunityRegistry {
|
||||
});
|
||||
}
|
||||
|
||||
/// Ask the workspace to close a community's panel.
|
||||
pub fn emit_close(&mut self, id: CommunityId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.defer_in(window, move |_this, _window, cx| {
|
||||
cx.emit(CommunityEvent::Close(id));
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a community owned by the current account and begin tracking it.
|
||||
pub fn create(&mut self, metadata: CommunityMetadata, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
@@ -228,26 +235,56 @@ impl CommunityRegistry {
|
||||
}
|
||||
|
||||
/// Replace the tracked communities with a freshly loaded set.
|
||||
///
|
||||
/// A community that survives the reload keeps its entity, so an open panel
|
||||
/// and a browsing sidebar stay pointed at a live community.
|
||||
fn track(&mut self, states: Vec<CommunityState>, cx: &mut Context<Self>) {
|
||||
self.observers.clear();
|
||||
self.communities.clear();
|
||||
self.index.clear();
|
||||
self.synced.clear();
|
||||
let mut communities = Vec::with_capacity(states.len());
|
||||
|
||||
for state in states {
|
||||
let id = state.id;
|
||||
let community = cx.new(|_| Community::new(state));
|
||||
|
||||
self.observers
|
||||
.push(cx.observe(&community, |this, _community, cx| {
|
||||
this.sync_subscriptions(cx);
|
||||
cx.notify();
|
||||
}));
|
||||
let community = match self.index.remove(&id) {
|
||||
Some(community) => {
|
||||
// The list can carry plane material the store does not.
|
||||
if community.read(cx).state() != &state {
|
||||
community.update(cx, |community, _cx| community.adopt(state));
|
||||
}
|
||||
|
||||
self.index.insert(id, community.clone());
|
||||
self.communities.push(community);
|
||||
community
|
||||
}
|
||||
None => {
|
||||
let community = cx.new(|_| Community::new(state));
|
||||
|
||||
self.observers.insert(
|
||||
id,
|
||||
cx.observe(&community, |this, _community, cx| {
|
||||
this.sync_subscriptions(cx);
|
||||
cx.notify();
|
||||
}),
|
||||
);
|
||||
|
||||
community
|
||||
}
|
||||
};
|
||||
|
||||
communities.push((id, community));
|
||||
}
|
||||
|
||||
// Whatever the index still holds is no longer in the list.
|
||||
let dropped: Vec<CommunityId> = self.index.keys().copied().collect();
|
||||
|
||||
for id in dropped {
|
||||
self.observers.remove(&id);
|
||||
self.synced.remove(&id);
|
||||
}
|
||||
|
||||
self.communities = communities
|
||||
.iter()
|
||||
.map(|(_, community)| community.clone())
|
||||
.collect();
|
||||
self.index = communities.into_iter().collect();
|
||||
|
||||
self.sync_subscriptions(cx);
|
||||
|
||||
// A backlog already in the database produces no notification, so fold it once.
|
||||
|
||||
@@ -92,9 +92,9 @@ pub fn community_of(subscription_id: &SubscriptionId) -> Option<CommunityId> {
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub async fn resolve_image(image: &ImageRef, cx: &AsyncApp) -> Result<PathBuf> {
|
||||
let url = Url::parse(&image.url).context("community image url")?;
|
||||
state::download_and_decrypt_to_cache(&url, &image.key, &image.nonce, &image.hash, cx).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
Reference in New Issue
Block a user