Start work on adding sub-channels in the UI

Max Brunsfeld and Mikayla created

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/collab_ui/src/panel.rs | 315 +++++++++++++++++++++---------------
1 file changed, 179 insertions(+), 136 deletions(-)

Detailed changes

crates/collab_ui/src/panel.rs 🔗

@@ -17,7 +17,10 @@ use gpui::{
         Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState,
         MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg,
     },
-    geometry::{rect::RectF, vector::vec2f},
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
     impl_actions,
     platform::{CursorStyle, MouseButton, PromptLevel},
     serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle,
@@ -42,9 +45,14 @@ struct RemoveChannel {
     channel_id: u64,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct NewChannel {
+    channel_id: u64,
+}
+
 actions!(collab_panel, [ToggleFocus]);
 
-impl_actions!(collab_panel, [RemoveChannel]);
+impl_actions!(collab_panel, [RemoveChannel, NewChannel]);
 
 const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
 
@@ -58,11 +66,12 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
     cx.add_action(CollabPanel::select_prev);
     cx.add_action(CollabPanel::confirm);
     cx.add_action(CollabPanel::remove_channel);
+    cx.add_action(CollabPanel::new_subchannel);
 }
 
 #[derive(Debug, Default)]
 pub struct ChannelEditingState {
-    root_channel: bool,
+    parent_id: Option<u64>,
 }
 
 pub struct CollabPanel {
@@ -74,7 +83,7 @@ pub struct CollabPanel {
     filter_editor: ViewHandle<Editor>,
     channel_name_editor: ViewHandle<Editor>,
     channel_editing_state: Option<ChannelEditingState>,
-    entries: Vec<ContactEntry>,
+    entries: Vec<ListEntry>,
     selection: Option<usize>,
     user_store: ModelHandle<UserStore>,
     channel_store: ModelHandle<ChannelStore>,
@@ -109,7 +118,7 @@ enum Section {
 }
 
 #[derive(Clone, Debug)]
-enum ContactEntry {
+enum ListEntry {
     Header(Section, usize),
     CallParticipant {
         user: Arc<User>,
@@ -125,10 +134,13 @@ enum ContactEntry {
         peer_id: PeerId,
         is_last: bool,
     },
-    ChannelInvite(Arc<Channel>),
     IncomingRequest(Arc<User>),
     OutgoingRequest(Arc<User>),
+    ChannelInvite(Arc<Channel>),
     Channel(Arc<Channel>),
+    ChannelEditor {
+        depth: usize,
+    },
     Contact {
         contact: Arc<Contact>,
         calling: bool,
@@ -166,7 +178,7 @@ impl CollabPanel {
                         this.selection = this
                             .entries
                             .iter()
-                            .position(|entry| !matches!(entry, ContactEntry::Header(_, _)));
+                            .position(|entry| !matches!(entry, ListEntry::Header(_, _)));
                     }
                 }
             })
@@ -184,6 +196,7 @@ impl CollabPanel {
             cx.subscribe(&channel_name_editor, |this, _, event, cx| {
                 if let editor::Event::Blurred = event {
                     this.take_editing_state(cx);
+                    this.update_entries(cx);
                     cx.notify();
                 }
             })
@@ -196,7 +209,7 @@ impl CollabPanel {
                     let current_project_id = this.project.read(cx).remote_id();
 
                     match &this.entries[ix] {
-                        ContactEntry::Header(section, depth) => {
+                        ListEntry::Header(section, depth) => {
                             let is_collapsed = this.collapsed_sections.contains(section);
                             this.render_header(
                                 *section,
@@ -207,7 +220,7 @@ impl CollabPanel {
                                 cx,
                             )
                         }
-                        ContactEntry::CallParticipant { user, is_pending } => {
+                        ListEntry::CallParticipant { user, is_pending } => {
                             Self::render_call_participant(
                                 user,
                                 *is_pending,
@@ -215,7 +228,7 @@ impl CollabPanel {
                                 &theme.collab_panel,
                             )
                         }
-                        ContactEntry::ParticipantProject {
+                        ListEntry::ParticipantProject {
                             project_id,
                             worktree_root_names,
                             host_user_id,
@@ -230,7 +243,7 @@ impl CollabPanel {
                             &theme.collab_panel,
                             cx,
                         ),
-                        ContactEntry::ParticipantScreen { peer_id, is_last } => {
+                        ListEntry::ParticipantScreen { peer_id, is_last } => {
                             Self::render_participant_screen(
                                 *peer_id,
                                 *is_last,
@@ -239,17 +252,17 @@ impl CollabPanel {
                                 cx,
                             )
                         }
-                        ContactEntry::Channel(channel) => {
+                        ListEntry::Channel(channel) => {
                             Self::render_channel(&*channel, &theme.collab_panel, is_selected, cx)
                         }
-                        ContactEntry::ChannelInvite(channel) => Self::render_channel_invite(
+                        ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
                             channel.clone(),
                             this.channel_store.clone(),
                             &theme.collab_panel,
                             is_selected,
                             cx,
                         ),
-                        ContactEntry::IncomingRequest(user) => Self::render_contact_request(
+                        ListEntry::IncomingRequest(user) => Self::render_contact_request(
                             user.clone(),
                             this.user_store.clone(),
                             &theme.collab_panel,
@@ -257,7 +270,7 @@ impl CollabPanel {
                             is_selected,
                             cx,
                         ),
-                        ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
+                        ListEntry::OutgoingRequest(user) => Self::render_contact_request(
                             user.clone(),
                             this.user_store.clone(),
                             &theme.collab_panel,
@@ -265,7 +278,7 @@ impl CollabPanel {
                             is_selected,
                             cx,
                         ),
-                        ContactEntry::Contact { contact, calling } => Self::render_contact(
+                        ListEntry::Contact { contact, calling } => Self::render_contact(
                             contact,
                             *calling,
                             &this.project,
@@ -273,6 +286,9 @@ impl CollabPanel {
                             is_selected,
                             cx,
                         ),
+                        ListEntry::ChannelEditor { depth } => {
+                            this.render_channel_editor(&theme.collab_panel, *depth, cx)
+                        }
                     }
                 });
 
@@ -369,13 +385,6 @@ impl CollabPanel {
         );
     }
 
-    fn is_editing_root_channel(&self) -> bool {
-        self.channel_editing_state
-            .as_ref()
-            .map(|state| state.root_channel)
-            .unwrap_or(false)
-    }
-
     fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
         let channel_store = self.channel_store.read(cx);
         let user_store = self.user_store.read(cx);
@@ -407,13 +416,13 @@ impl CollabPanel {
                 ));
                 if !matches.is_empty() {
                     let user_id = user.id;
-                    participant_entries.push(ContactEntry::CallParticipant {
+                    participant_entries.push(ListEntry::CallParticipant {
                         user,
                         is_pending: false,
                     });
                     let mut projects = room.local_participant().projects.iter().peekable();
                     while let Some(project) = projects.next() {
-                        participant_entries.push(ContactEntry::ParticipantProject {
+                        participant_entries.push(ListEntry::ParticipantProject {
                             project_id: project.id,
                             worktree_root_names: project.worktree_root_names.clone(),
                             host_user_id: user_id,
@@ -444,13 +453,13 @@ impl CollabPanel {
             for mat in matches {
                 let user_id = mat.candidate_id as u64;
                 let participant = &room.remote_participants()[&user_id];
-                participant_entries.push(ContactEntry::CallParticipant {
+                participant_entries.push(ListEntry::CallParticipant {
                     user: participant.user.clone(),
                     is_pending: false,
                 });
                 let mut projects = participant.projects.iter().peekable();
                 while let Some(project) = projects.next() {
-                    participant_entries.push(ContactEntry::ParticipantProject {
+                    participant_entries.push(ListEntry::ParticipantProject {
                         project_id: project.id,
                         worktree_root_names: project.worktree_root_names.clone(),
                         host_user_id: participant.user.id,
@@ -458,7 +467,7 @@ impl CollabPanel {
                     });
                 }
                 if !participant.video_tracks.is_empty() {
-                    participant_entries.push(ContactEntry::ParticipantScreen {
+                    participant_entries.push(ListEntry::ParticipantScreen {
                         peer_id: participant.peer_id,
                         is_last: true,
                     });
@@ -486,22 +495,20 @@ impl CollabPanel {
                 &Default::default(),
                 executor.clone(),
             ));
-            participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant {
+            participant_entries.extend(matches.iter().map(|mat| ListEntry::CallParticipant {
                 user: room.pending_participants()[mat.candidate_id].clone(),
                 is_pending: true,
             }));
 
             if !participant_entries.is_empty() {
-                self.entries
-                    .push(ContactEntry::Header(Section::ActiveCall, 0));
+                self.entries.push(ListEntry::Header(Section::ActiveCall, 0));
                 if !self.collapsed_sections.contains(&Section::ActiveCall) {
                     self.entries.extend(participant_entries);
                 }
             }
         }
 
-        self.entries
-            .push(ContactEntry::Header(Section::Channels, 0));
+        self.entries.push(ListEntry::Header(Section::Channels, 0));
 
         let channels = channel_store.channels();
         if !channels.is_empty() {
@@ -525,15 +532,25 @@ impl CollabPanel {
                 &Default::default(),
                 executor.clone(),
             ));
-            self.entries.extend(
-                matches
-                    .iter()
-                    .map(|mat| ContactEntry::Channel(channels[mat.candidate_id].clone())),
-            );
+            if let Some(state) = &self.channel_editing_state {
+                if state.parent_id.is_none() {
+                    self.entries.push(ListEntry::ChannelEditor { depth: 0 });
+                }
+            }
+            for mat in matches {
+                let channel = &channels[mat.candidate_id];
+                self.entries.push(ListEntry::Channel(channel.clone()));
+                if let Some(state) = &self.channel_editing_state {
+                    if state.parent_id == Some(channel.id) {
+                        self.entries.push(ListEntry::ChannelEditor {
+                            depth: channel.depth + 1,
+                        });
+                    }
+                }
+            }
         }
 
-        self.entries
-            .push(ContactEntry::Header(Section::Contacts, 0));
+        self.entries.push(ListEntry::Header(Section::Contacts, 0));
 
         let mut request_entries = Vec::new();
         let channel_invites = channel_store.channel_invitations();
@@ -556,9 +573,9 @@ impl CollabPanel {
                 executor.clone(),
             ));
             request_entries.extend(
-                matches.iter().map(|mat| {
-                    ContactEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
-                }),
+                matches
+                    .iter()
+                    .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())),
             );
         }
 
@@ -587,7 +604,7 @@ impl CollabPanel {
             request_entries.extend(
                 matches
                     .iter()
-                    .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
+                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
             );
         }
 
@@ -616,13 +633,12 @@ impl CollabPanel {
             request_entries.extend(
                 matches
                     .iter()
-                    .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
+                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
             );
         }
 
         if !request_entries.is_empty() {
-            self.entries
-                .push(ContactEntry::Header(Section::Requests, 1));
+            self.entries.push(ListEntry::Header(Section::Requests, 1));
             if !self.collapsed_sections.contains(&Section::Requests) {
                 self.entries.append(&mut request_entries);
             }
@@ -668,12 +684,12 @@ impl CollabPanel {
                 (offline_contacts, Section::Offline),
             ] {
                 if !matches.is_empty() {
-                    self.entries.push(ContactEntry::Header(section, 1));
+                    self.entries.push(ListEntry::Header(section, 1));
                     if !self.collapsed_sections.contains(&section) {
                         let active_call = &ActiveCall::global(cx).read(cx);
                         for mat in matches {
                             let contact = &contacts[mat.candidate_id];
-                            self.entries.push(ContactEntry::Contact {
+                            self.entries.push(ListEntry::Contact {
                                 contact: contact.clone(),
                                 calling: active_call.pending_invites().contains(&contact.user.id),
                             });
@@ -1072,15 +1088,7 @@ impl CollabPanel {
                     render_icon_button(&theme.collab_panel.add_contact_button, "icons/plus_16.svg")
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, this, cx| {
-                    if this.channel_editing_state.is_none() {
-                        this.channel_editing_state =
-                            Some(ChannelEditingState { root_channel: true });
-                    }
-
-                    cx.focus(this.channel_name_editor.as_any());
-                    cx.notify();
-                })
+                .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
                 .with_tooltip::<AddChannel>(
                     0,
                     "Add or join a channel".into(),
@@ -1092,13 +1100,6 @@ impl CollabPanel {
             _ => None,
         };
 
-        let addition = match section {
-            Section::Channels if self.is_editing_root_channel() => {
-                Some(ChildView::new(self.channel_name_editor.as_any(), cx))
-            }
-            _ => None,
-        };
-
         let can_collapse = depth > 0;
         let icon_size = (&theme.collab_panel).section_icon_size;
         MouseEventHandler::<Header, Self>::new(section as usize, cx, |state, _| {
@@ -1112,44 +1113,40 @@ impl CollabPanel {
                 &theme.collab_panel.header_row
             };
 
-            Flex::column()
-                .with_child(
-                    Flex::row()
-                        .with_children(if can_collapse {
-                            Some(
-                                Svg::new(if is_collapsed {
-                                    "icons/chevron_right_8.svg"
-                                } else {
-                                    "icons/chevron_down_8.svg"
-                                })
-                                .with_color(header_style.text.color)
-                                .constrained()
-                                .with_max_width(icon_size)
-                                .with_max_height(icon_size)
-                                .aligned()
-                                .constrained()
-                                .with_width(icon_size)
-                                .contained()
-                                .with_margin_right(
-                                    theme.collab_panel.contact_username.container.margin.left,
-                                ),
-                            )
+            Flex::row()
+                .with_children(if can_collapse {
+                    Some(
+                        Svg::new(if is_collapsed {
+                            "icons/chevron_right_8.svg"
                         } else {
-                            None
+                            "icons/chevron_down_8.svg"
                         })
-                        .with_child(
-                            Label::new(text, header_style.text.clone())
-                                .aligned()
-                                .left()
-                                .flex(1., true),
-                        )
-                        .with_children(button.map(|button| button.aligned().right()))
+                        .with_color(header_style.text.color)
+                        .constrained()
+                        .with_max_width(icon_size)
+                        .with_max_height(icon_size)
+                        .aligned()
                         .constrained()
-                        .with_height(theme.collab_panel.row_height)
+                        .with_width(icon_size)
                         .contained()
-                        .with_style(header_style.container),
+                        .with_margin_right(
+                            theme.collab_panel.contact_username.container.margin.left,
+                        ),
+                    )
+                } else {
+                    None
+                })
+                .with_child(
+                    Label::new(text, header_style.text.clone())
+                        .aligned()
+                        .left()
+                        .flex(1., true),
                 )
-                .with_children(addition)
+                .with_children(button.map(|button| button.aligned().right()))
+                .constrained()
+                .with_height(theme.collab_panel.row_height)
+                .contained()
+                .with_style(header_style.container)
         })
         .with_cursor_style(CursorStyle::PointingHand)
         .on_click(MouseButton::Left, move |_, this, cx| {
@@ -1258,6 +1255,15 @@ impl CollabPanel {
         event_handler.into_any()
     }
 
+    fn render_channel_editor(
+        &self,
+        theme: &theme::CollabPanel,
+        depth: usize,
+        cx: &AppContext,
+    ) -> AnyElement<Self> {
+        ChildView::new(&self.channel_name_editor, cx).into_any()
+    }
+
     fn render_channel(
         channel: &Channel,
         theme: &theme::CollabPanel,
@@ -1285,22 +1291,13 @@ impl CollabPanel {
                 .with_height(theme.row_height)
                 .contained()
                 .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
+                .with_margin_left(10. * channel.depth as f32)
         })
         .on_click(MouseButton::Left, move |_, this, cx| {
             this.join_channel(channel_id, cx);
         })
         .on_click(MouseButton::Right, move |e, this, cx| {
-            this.context_menu.update(cx, |context_menu, cx| {
-                context_menu.show(
-                    e.position,
-                    gpui::elements::AnchorCorner::BottomLeft,
-                    vec![ContextMenuItem::action(
-                        "Remove Channel",
-                        RemoveChannel { channel_id },
-                    )],
-                    cx,
-                );
-            });
+            this.deploy_channel_context_menu(e.position, channel_id, cx);
         })
         .into_any()
     }
@@ -1489,6 +1486,25 @@ impl CollabPanel {
             .into_any()
     }
 
+    fn deploy_channel_context_menu(
+        &mut self,
+        position: Vector2F,
+        channel_id: u64,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.context_menu.update(cx, |context_menu, cx| {
+            context_menu.show(
+                position,
+                gpui::elements::AnchorCorner::BottomLeft,
+                vec![
+                    ContextMenuItem::action("New Channel", NewChannel { channel_id }),
+                    ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }),
+                ],
+                cx,
+            );
+        });
+    }
+
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
         let mut did_clear = self.filter_editor.update(cx, |editor, cx| {
             if editor.buffer().read(cx).len(cx) > 0 {
@@ -1553,15 +1569,15 @@ impl CollabPanel {
         if let Some(selection) = self.selection {
             if let Some(entry) = self.entries.get(selection) {
                 match entry {
-                    ContactEntry::Header(section, _) => {
+                    ListEntry::Header(section, _) => {
                         self.toggle_expanded(*section, cx);
                     }
-                    ContactEntry::Contact { contact, calling } => {
+                    ListEntry::Contact { contact, calling } => {
                         if contact.online && !contact.busy && !calling {
                             self.call(contact.user.id, Some(self.project.clone()), cx);
                         }
                     }
-                    ContactEntry::ParticipantProject {
+                    ListEntry::ParticipantProject {
                         project_id,
                         host_user_id,
                         ..
@@ -1577,7 +1593,7 @@ impl CollabPanel {
                             .detach_and_log_err(cx);
                         }
                     }
-                    ContactEntry::ParticipantScreen { peer_id, .. } => {
+                    ListEntry::ParticipantScreen { peer_id, .. } => {
                         if let Some(workspace) = self.workspace.upgrade(cx) {
                             workspace.update(cx, |workspace, cx| {
                                 workspace.open_shared_screen(*peer_id, cx)
@@ -1587,9 +1603,9 @@ impl CollabPanel {
                     _ => {}
                 }
             }
-        } else if let Some((_editing_state, channel_name)) = self.take_editing_state(cx) {
+        } else if let Some((editing_state, channel_name)) = self.take_editing_state(cx) {
             let create_channel = self.channel_store.update(cx, |channel_store, cx| {
-                channel_store.create_channel(&channel_name, None)
+                channel_store.create_channel(&channel_name, editing_state.parent_id)
             });
 
             cx.foreground()
@@ -1623,6 +1639,28 @@ impl CollabPanel {
         }
     }
 
+    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
+        if self.channel_editing_state.is_none() {
+            self.channel_editing_state = Some(ChannelEditingState { parent_id: None });
+            self.update_entries(cx);
+        }
+
+        cx.focus(self.channel_name_editor.as_any());
+        cx.notify();
+    }
+
+    fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
+        if self.channel_editing_state.is_none() {
+            self.channel_editing_state = Some(ChannelEditingState {
+                parent_id: Some(action.channel_id),
+            });
+            self.update_entries(cx);
+        }
+
+        cx.focus(self.channel_name_editor.as_any());
+        cx.notify();
+    }
+
     fn remove_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
         let channel_id = action.channel_id;
         let channel_store = self.channel_store.clone();
@@ -1838,9 +1876,9 @@ impl Panel for CollabPanel {
     }
 }
 
-impl ContactEntry {
+impl ListEntry {
     fn is_selectable(&self) -> bool {
-        if let ContactEntry::Header(_, 0) = self {
+        if let ListEntry::Header(_, 0) = self {
             false
         } else {
             true
@@ -1848,24 +1886,24 @@ impl ContactEntry {
     }
 }
 
-impl PartialEq for ContactEntry {
+impl PartialEq for ListEntry {
     fn eq(&self, other: &Self) -> bool {
         match self {
-            ContactEntry::Header(section_1, depth_1) => {
-                if let ContactEntry::Header(section_2, depth_2) = other {
+            ListEntry::Header(section_1, depth_1) => {
+                if let ListEntry::Header(section_2, depth_2) = other {
                     return section_1 == section_2 && depth_1 == depth_2;
                 }
             }
-            ContactEntry::CallParticipant { user: user_1, .. } => {
-                if let ContactEntry::CallParticipant { user: user_2, .. } = other {
+            ListEntry::CallParticipant { user: user_1, .. } => {
+                if let ListEntry::CallParticipant { user: user_2, .. } = other {
                     return user_1.id == user_2.id;
                 }
             }
-            ContactEntry::ParticipantProject {
+            ListEntry::ParticipantProject {
                 project_id: project_id_1,
                 ..
             } => {
-                if let ContactEntry::ParticipantProject {
+                if let ListEntry::ParticipantProject {
                     project_id: project_id_2,
                     ..
                 } = other
@@ -1873,46 +1911,51 @@ impl PartialEq for ContactEntry {
                     return project_id_1 == project_id_2;
                 }
             }
-            ContactEntry::ParticipantScreen {
+            ListEntry::ParticipantScreen {
                 peer_id: peer_id_1, ..
             } => {
-                if let ContactEntry::ParticipantScreen {
+                if let ListEntry::ParticipantScreen {
                     peer_id: peer_id_2, ..
                 } = other
                 {
                     return peer_id_1 == peer_id_2;
                 }
             }
-            ContactEntry::Channel(channel_1) => {
-                if let ContactEntry::Channel(channel_2) = other {
+            ListEntry::Channel(channel_1) => {
+                if let ListEntry::Channel(channel_2) = other {
                     return channel_1.id == channel_2.id;
                 }
             }
-            ContactEntry::ChannelInvite(channel_1) => {
-                if let ContactEntry::ChannelInvite(channel_2) = other {
+            ListEntry::ChannelInvite(channel_1) => {
+                if let ListEntry::ChannelInvite(channel_2) = other {
                     return channel_1.id == channel_2.id;
                 }
             }
-            ContactEntry::IncomingRequest(user_1) => {
-                if let ContactEntry::IncomingRequest(user_2) = other {
+            ListEntry::IncomingRequest(user_1) => {
+                if let ListEntry::IncomingRequest(user_2) = other {
                     return user_1.id == user_2.id;
                 }
             }
-            ContactEntry::OutgoingRequest(user_1) => {
-                if let ContactEntry::OutgoingRequest(user_2) = other {
+            ListEntry::OutgoingRequest(user_1) => {
+                if let ListEntry::OutgoingRequest(user_2) = other {
                     return user_1.id == user_2.id;
                 }
             }
-            ContactEntry::Contact {
+            ListEntry::Contact {
                 contact: contact_1, ..
             } => {
-                if let ContactEntry::Contact {
+                if let ListEntry::Contact {
                     contact: contact_2, ..
                 } = other
                 {
                     return contact_1.user.id == contact_2.user.id;
                 }
             }
+            ListEntry::ChannelEditor { depth } => {
+                if let ListEntry::ChannelEditor { depth: other_depth } = other {
+                    return depth == other_depth;
+                }
+            }
         }
         false
     }