Add persistence to the `Show Occupied Channels` collab toggle (#53029)

Joseph T. Lyons created

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.

Change summary

crates/collab_ui/src/collab_panel.rs | 74 +++++++++++++++++++++++------
1 file changed, 58 insertions(+), 16 deletions(-)

Detailed changes

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<dyn Fs>,
     focus_handle: FocusHandle,
     channel_clipboard: Option<ChannelMoveClipboard>,
-    pending_serialization: Task<Option<()>>,
+    pending_panel_serialization: Task<Option<()>>,
+    pending_favorites_serialization: Task<Option<()>>,
+    pending_filter_serialization: Task<Option<()>>,
     context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
     list_state: ListState,
     filter_editor: Entity<Editor>,
@@ -260,7 +265,7 @@ pub struct CollabPanel {
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
     collapsed_channels: Vec<ChannelId>,
-    filter_active_channels: bool,
+    filter_occupied_channels: bool,
     workspace: WeakEntity<Workspace>,
 }
 
@@ -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<ChannelId> = KeyValueStore::global(cx)
-                .read_kvp("favorite_channels")
+                .read_kvp(FAVORITE_CHANNELS_KEY)
                 .ok()
                 .flatten()
                 .and_then(|json| serde_json::from_str::<Vec<u64>>(&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<Self>) {
+        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<Self>) {
         let favorite_ids: Vec<u64> = 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"