From 134dec8f95bf884db5e27270f9beabd00ef4132d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 2 Apr 2026 23:06:45 -0400 Subject: [PATCH] Add persistence to the `Show Occupied Channels` collab toggle (#53029) This PR adds persistence to the toggle state for the `Show Occupied Channels` filter. Also, while driving by, I: - renamed a few variables to use `occupied` over `active`, which should've happened in https://github.com/zed-industries/zed/pull/52531. - extracted `"favorite_channels"` into a global Self-Review Checklist: - [X] I've reviewed my own diff for quality, security, and reliability - [X] Unsafe blocks (if any) have justifying comments - [X] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [X] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - Added persistence to the `Show Occupied Channels` collab toggle. --- crates/collab_ui/src/collab_panel.rs | 74 ++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index d16db59ea4ae2d766018dfc03c245839e4862cb4..8d0cdf351163dadf0ac8cbf6a8dc04886f30f583 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -44,6 +44,9 @@ use workspace::{ notifications::{DetachAndPromptErr, NotifyResultExt}, }; +const FILTER_OCCUPIED_CHANNELS_KEY: &str = "filter_occupied_channels"; +const FAVORITE_CHANNELS_KEY: &str = "favorite_channels"; + actions!( collab_panel, [ @@ -244,7 +247,9 @@ pub struct CollabPanel { fs: Arc, focus_handle: FocusHandle, channel_clipboard: Option, - pending_serialization: Task>, + pending_panel_serialization: Task>, + pending_favorites_serialization: Task>, + pending_filter_serialization: Task>, context_menu: Option<(Entity, Point, Subscription)>, list_state: ListState, filter_editor: Entity, @@ -260,7 +265,7 @@ pub struct CollabPanel { subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, - filter_active_channels: bool, + filter_occupied_channels: bool, workspace: WeakEntity, } @@ -378,7 +383,9 @@ impl CollabPanel { focus_handle: cx.focus_handle(), channel_clipboard: None, fs: workspace.app_state().fs.clone(), - pending_serialization: Task::ready(None), + pending_panel_serialization: Task::ready(None), + pending_favorites_serialization: Task::ready(None), + pending_filter_serialization: Task::ready(None), context_menu: None, list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)), channel_name_editor, @@ -393,7 +400,7 @@ impl CollabPanel { match_candidates: Vec::default(), collapsed_sections: vec![Section::Offline], collapsed_channels: Vec::default(), - filter_active_channels: false, + filter_occupied_channels: false, workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), }; @@ -474,8 +481,22 @@ impl CollabPanel { }); } + let filter_occupied_channels = KeyValueStore::global(cx) + .read_kvp(FILTER_OCCUPIED_CHANNELS_KEY) + .ok() + .flatten() + .is_some(); + + panel.update(cx, |panel, cx| { + panel.filter_occupied_channels = filter_occupied_channels; + + if filter_occupied_channels { + panel.update_entries(false, cx); + } + }); + let favorites: Vec = KeyValueStore::global(cx) - .read_kvp("favorite_channels") + .read_kvp(FAVORITE_CHANNELS_KEY) .ok() .flatten() .and_then(|json| serde_json::from_str::>(&json).ok()) @@ -520,7 +541,7 @@ impl CollabPanel { }; let kvp = KeyValueStore::global(cx); - self.pending_serialization = cx.background_spawn( + self.pending_panel_serialization = cx.background_spawn( async move { kvp.write_kvp( serialization_key, @@ -780,14 +801,14 @@ impl CollabPanel { channels.retain(|chan| channel_ids_of_matches_or_parents.contains(&chan.id)); - if self.filter_active_channels { - let active_channel_ids_or_ancestors: HashSet<_> = channel_store + if self.filter_occupied_channels { + let occupied_channel_ids_or_ancestors: HashSet<_> = channel_store .ordered_channels() .map(|(_, channel)| channel) .filter(|channel| !channel_store.channel_participants(channel.id).is_empty()) .flat_map(|channel| channel.parent_path.iter().copied().chain(Some(channel.id))) .collect(); - channels.retain(|channel| active_channel_ids_or_ancestors.contains(&channel.id)); + channels.retain(|channel| occupied_channel_ids_or_ancestors.contains(&channel.id)); } if let Some(state) = &self.channel_editing_state @@ -796,7 +817,7 @@ impl CollabPanel { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } - let should_respect_collapse = query.is_empty() && !self.filter_active_channels; + let should_respect_collapse = query.is_empty() && !self.filter_occupied_channels; let mut collapse_depth = None; for (idx, channel) in channels.into_iter().enumerate() { @@ -1970,6 +1991,26 @@ impl CollabPanel { self.channel_store.read(cx).is_channel_favorited(channel_id) } + fn persist_filter_occupied_channels(&mut self, cx: &mut Context) { + let is_enabled = self.filter_occupied_channels; + let kvp_store = KeyValueStore::global(cx); + self.pending_filter_serialization = cx.background_spawn( + async move { + if is_enabled { + kvp_store + .write_kvp(FILTER_OCCUPIED_CHANNELS_KEY.to_string(), "1".to_string()) + .await?; + } else { + kvp_store + .delete_kvp(FILTER_OCCUPIED_CHANNELS_KEY.to_string()) + .await?; + } + anyhow::Ok(()) + } + .log_err(), + ); + } + fn persist_favorites(&mut self, cx: &mut Context) { let favorite_ids: Vec = self .channel_store @@ -1979,11 +2020,11 @@ impl CollabPanel { .map(|id| id.0) .collect(); let kvp_store = KeyValueStore::global(cx); - self.pending_serialization = cx.background_spawn( + self.pending_favorites_serialization = cx.background_spawn( async move { let json = serde_json::to_string(&favorite_ids)?; kvp_store - .write_kvp("favorite_channels".to_string(), json) + .write_kvp(FAVORITE_CHANNELS_KEY.to_string(), json) .await?; anyhow::Ok(()) } @@ -2843,14 +2884,15 @@ impl CollabPanel { Some( h_flex() .child( - IconButton::new("filter-active-channels", IconName::ListFilter) + IconButton::new("filter-occupied-channels", IconName::ListFilter) .icon_size(IconSize::Small) - .toggle_state(self.filter_active_channels) + .toggle_state(self.filter_occupied_channels) .on_click(cx.listener(|this, _, _window, cx| { - this.filter_active_channels = !this.filter_active_channels; + this.filter_occupied_channels = !this.filter_occupied_channels; this.update_entries(true, cx); + this.persist_filter_occupied_channels(cx); })) - .tooltip(Tooltip::text(if self.filter_active_channels { + .tooltip(Tooltip::text(if self.filter_occupied_channels { "Show All Channels" } else { "Show Occupied Channels"