mod channel_modal;
mod contact_finder;

use self::channel_modal::ChannelModal;
use crate::{CollaborationPanelSettings, channel_view::ChannelView};
use anyhow::Context as _;
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore};
use client::{ChannelId, Client, Contact, Notification, User, UserStore};
use collections::{HashMap, HashSet};
use contact_finder::ContactFinder;
use db::kvp::KeyValueStore;
use editor::{Editor, EditorElement, EditorStyle};
use feature_flags::{AutoWatchFeatureFlag, FeatureFlagAppExt as _};
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
    AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, DismissEvent, Div,
    Empty, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, KeyContext, ListOffset,
    ListState, MouseDownEvent, Pixels, Point, PromptLevel, SharedString, Subscription, Task,
    TextStyle, WeakEntity, Window, actions, anchored, canvas, deferred, div, fill, list, point,
    prelude::*, px,
};

use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
use project::{Fs, Project};
use rpc::{
    ErrorCode, ErrorExt,
    proto::{self, ChannelVisibility, PeerId, reorder_channel::Direction},
};
use serde::{Deserialize, Serialize};
use settings::Settings;
use smallvec::SmallVec;
use std::{mem, sync::Arc, time::Duration};
use theme::ActiveTheme;
use theme_settings::ThemeSettings;
use ui::{
    Avatar, AvatarAvailabilityIndicator, CollabNotification, ContextMenu, CopyButton, Facepile,
    HighlightedLabel, IconButtonShape, Indicator, ListHeader, ListItem, Tab, TintColor, Tooltip,
    prelude::*, tooltip_container,
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::{
    AutoWatch, CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes,
    OpenChannelNotesById, ScreenShare, ShareProject, Workspace,
    dock::{DockPosition, Panel, PanelEvent},
    notifications::{
        DetachAndPromptErr, Notification as WorkspaceNotification, NotificationId, NotifyResultExt,
        SuppressEvent,
    },
};

const FILTER_OCCUPIED_CHANNELS_KEY: &str = "filter_occupied_channels";
const FAVORITE_CHANNELS_KEY: &str = "favorite_channels";

actions!(
    collab_panel,
    [
        /// Toggles the collab panel.
        Toggle,
        /// Toggles focus on the collaboration panel.
        ToggleFocus,
        /// Removes the selected channel or contact.
        Remove,
        /// Opens the context menu for the selected item.
        Secondary,
        /// Collapses the selected channel in the tree view.
        CollapseSelectedChannel,
        /// Expands the selected channel in the tree view.
        ExpandSelectedChannel,
        /// Opens the meeting notes for the selected channel in the panel.
        ///
        /// Use `collab::OpenChannelNotes` to open the channel notes for the current call.
        OpenSelectedChannelNotes,
        /// Toggles whether the selected channel is in the Favorites section.
        ToggleSelectedChannelFavorite,
        /// Starts moving a channel to a new location.
        StartMoveChannel,
        /// Moves the selected item to the current location.
        MoveSelected,
        /// Inserts a space character in the filter input.
        InsertSpace,
        /// Moves the selected channel up in the list.
        MoveChannelUp,
        /// Moves the selected channel down in the list.
        MoveChannelDown,
    ]
);

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct ChannelMoveClipboard {
    channel_id: ChannelId,
}

const COLLABORATION_PANEL_KEY: &str = "CollaborationPanel";
const TOAST_DURATION: Duration = Duration::from_secs(5);

pub fn init(cx: &mut App) {
    cx.observe_new(|workspace: &mut Workspace, _, _| {
        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
            workspace.toggle_panel_focus::<CollabPanel>(window, cx);
            if let Some(collab_panel) = workspace.panel::<CollabPanel>(cx) {
                collab_panel.update(cx, |panel, cx| {
                    panel.filter_editor.update(cx, |editor, cx| {
                        if editor.snapshot(window, cx).is_focused() {
                            editor.select_all(&Default::default(), window, cx);
                        }
                    });
                })
            }
        });
        workspace.register_action(|workspace, _: &Toggle, window, cx| {
            if !workspace.toggle_panel_focus::<CollabPanel>(window, cx) {
                workspace.close_panel::<CollabPanel>(window, cx);
            }
        });
        workspace.register_action(|_, _: &OpenChannelNotes, window, cx| {
            let channel_id = ActiveCall::global(cx)
                .read(cx)
                .room()
                .and_then(|room| room.read(cx).channel_id());

            if let Some(channel_id) = channel_id {
                let workspace = cx.entity();
                window.defer(cx, move |window, cx| {
                    ChannelView::open(channel_id, None, workspace, window, cx)
                        .detach_and_log_err(cx)
                });
            }
        });
        workspace.register_action(|_, action: &OpenChannelNotesById, window, cx| {
            let channel_id = client::ChannelId(action.channel_id);
            let workspace = cx.entity();
            window.defer(cx, move |window, cx| {
                ChannelView::open(channel_id, None, workspace, window, cx).detach_and_log_err(cx)
            });
        });
        // TODO: make it possible to bind this one to a held key for push to talk?
        // how to make "toggle_on_modifiers_press" contextual?
        workspace.register_action(|_, _: &Mute, _, cx| title_bar::collab::toggle_mute(cx));
        workspace.register_action(|_, _: &Deafen, _, cx| title_bar::collab::toggle_deafen(cx));
        workspace.register_action(|_, _: &LeaveCall, window, cx| {
            CollabPanel::leave_call(window, cx);
        });
        workspace.register_action(|workspace, _: &CopyRoomId, window, cx| {
            use workspace::notifications::{NotificationId, NotifyTaskExt as _};

            struct RoomIdCopiedToast;

            if let Some(room) = ActiveCall::global(cx).read(cx).room() {
                let romo_id_fut = room.read(cx).room_id();
                let workspace_handle = cx.weak_entity();
                cx.spawn(async move |workspace, cx| {
                    let room_id = romo_id_fut.await.context("Failed to get livekit room")?;
                    workspace.update(cx, |workspace, cx| {
                        cx.write_to_clipboard(ClipboardItem::new_string(room_id));
                        workspace.show_toast(
                            workspace::Toast::new(
                                NotificationId::unique::<RoomIdCopiedToast>(),
                                "Room ID copied to clipboard",
                            )
                            .autohide(),
                            cx,
                        );
                    })
                })
                .detach_and_notify_err(workspace_handle, window, cx);
            } else {
                workspace.show_error(&"There’s no active call; join one first.", cx);
            }
        });
        workspace.register_action(|workspace, _: &ShareProject, window, cx| {
            let project = workspace.project().clone();
            println!("{project:?}");
            window.defer(cx, move |_window, cx| {
                ActiveCall::global(cx).update(cx, move |call, cx| {
                    if let Some(room) = call.room() {
                        println!("{room:?}");
                        if room.read(cx).is_sharing_project() {
                            call.unshare_project(project, cx).ok();
                        } else {
                            call.share_project(project, cx).detach_and_log_err(cx);
                        }
                    }
                });
            });
        });
        // TODO(jk): Is this action ever triggered?
        workspace.register_action(|_, _: &ScreenShare, window, cx| {
            let room = ActiveCall::global(cx).read(cx).room().cloned();
            if let Some(room) = room {
                window.defer(cx, move |_window, cx| {
                    room.update(cx, |room, cx| {
                        if room.is_sharing_screen() {
                            room.unshare_screen(true, cx).ok();
                        } else {
                            #[cfg(target_os = "linux")]
                            let is_wayland = gpui::guess_compositor() == "Wayland";
                            #[cfg(not(target_os = "linux"))]
                            let is_wayland = false;

                            #[cfg(target_os = "linux")]
                            {
                                if is_wayland {
                                    room.share_screen_wayland(cx).detach_and_log_err(cx);
                                }
                            }
                            if !is_wayland {
                                let sources = cx.screen_capture_sources();

                                cx.spawn(async move |room, cx| {
                                    let sources = sources.await??;
                                    let first = sources.into_iter().next();
                                    if let Some(first) = first {
                                        room.update(cx, |room, cx| room.share_screen(first, cx))?
                                            .await
                                    } else {
                                        Ok(())
                                    }
                                })
                                .detach_and_log_err(cx);
                            }
                        };
                    });
                });
            }
        });
    })
    .detach();
}

#[derive(Debug)]
pub enum ChannelEditingState {
    Create {
        location: Option<ChannelId>,
        pending_name: Option<String>,
    },
    Rename {
        location: ChannelId,
        pending_name: Option<String>,
    },
}

impl ChannelEditingState {
    fn pending_name(&self) -> Option<String> {
        match self {
            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
        }
    }
}

pub struct CollabPanel {
    fs: Arc<dyn Fs>,
    focus_handle: FocusHandle,
    channel_clipboard: Option<ChannelMoveClipboard>,
    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>,
    channel_name_editor: Entity<Editor>,
    channel_editing_state: Option<ChannelEditingState>,
    entries: Vec<ListEntry>,
    selection: Option<usize>,
    channel_store: Entity<ChannelStore>,
    user_store: Entity<UserStore>,
    client: Arc<Client>,
    project: Entity<Project>,
    match_candidates: Vec<StringMatchCandidate>,
    subscriptions: Vec<Subscription>,
    collapsed_sections: Vec<Section>,
    collapsed_channels: Vec<ChannelId>,
    filter_occupied_channels: bool,
    workspace: WeakEntity<Workspace>,
    notification_store: Entity<NotificationStore>,
    current_notification_toast: Option<(u64, Task<()>)>,
    mark_as_read_tasks: HashMap<u64, Task<anyhow::Result<()>>>,
}

#[derive(Serialize, Deserialize)]
struct SerializedCollabPanel {
    collapsed_channels: Option<Vec<u64>>,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
enum Section {
    ActiveCall,
    FavoriteChannels,
    Channels,
    ChannelInvites,
    ContactRequests,
    Contacts,
    Online,
    Offline,
}

#[derive(Clone, Debug)]
enum ListEntry {
    Header(Section),
    CallParticipant {
        user: Arc<User>,
        peer_id: Option<PeerId>,
        is_pending: bool,
        role: proto::ChannelRole,
    },
    ParticipantProject {
        project_id: u64,
        worktree_root_names: Vec<String>,
        host_user_id: u64,
        is_last: bool,
    },
    ParticipantScreen {
        peer_id: Option<PeerId>,
        is_last: bool,
    },
    IncomingRequest(Arc<User>),
    OutgoingRequest(Arc<User>),
    ChannelInvite(Arc<Channel>),
    Channel {
        channel: Arc<Channel>,
        depth: usize,
        has_children: bool,
        is_favorite: bool,
        // `None` when the channel is a parent of a matched channel.
        string_match: Option<StringMatch>,
    },
    ChannelNotes {
        channel_id: ChannelId,
    },
    ChannelEditor {
        depth: usize,
    },
    Contact {
        contact: Arc<Contact>,
        calling: bool,
    },
    ContactPlaceholder,
}

impl CollabPanel {
    pub fn new(
        workspace: &mut Workspace,
        window: &mut Window,
        cx: &mut Context<Workspace>,
    ) -> Entity<Self> {
        cx.new(|cx| {
            let filter_editor = cx.new(|cx| {
                let mut editor = Editor::single_line(window, cx);
                editor.set_placeholder_text("Search channels…", window, cx);
                editor
            });

            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
                if let editor::EditorEvent::BufferEdited = event {
                    let query = this.filter_editor.read(cx).text(cx);
                    if !query.is_empty() {
                        this.selection.take();
                    }
                    this.update_entries(true, cx);
                    if !query.is_empty() {
                        this.selection = this
                            .entries
                            .iter()
                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
                    }
                }
            })
            .detach();

            let channel_name_editor = cx.new(|cx| Editor::single_line(window, cx));

            cx.subscribe_in(
                &channel_name_editor,
                window,
                |this: &mut Self, _, event, window, cx| {
                    if let editor::EditorEvent::Blurred = event {
                        if let Some(state) = &this.channel_editing_state
                            && state.pending_name().is_some()
                        {
                            return;
                        }
                        this.take_editing_state(window, cx);
                        this.update_entries(false, cx);
                        cx.notify();
                    }
                },
            )
            .detach();

            let mut this = Self {
                focus_handle: cx.focus_handle(),
                channel_clipboard: None,
                fs: workspace.app_state().fs.clone(),
                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,
                filter_editor,
                entries: Vec::default(),
                channel_editing_state: None,
                selection: None,
                channel_store: ChannelStore::global(cx),
                notification_store: NotificationStore::global(cx),
                current_notification_toast: None,
                mark_as_read_tasks: HashMap::default(),
                user_store: workspace.user_store().clone(),
                project: workspace.project().clone(),
                subscriptions: Vec::default(),
                match_candidates: Vec::default(),
                collapsed_sections: vec![Section::Offline],
                collapsed_channels: Vec::default(),
                filter_occupied_channels: false,
                workspace: workspace.weak_handle(),
                client: workspace.app_state().client.clone(),
            };

            this.update_entries(false, cx);

            let active_call = ActiveCall::global(cx);
            this.subscriptions
                .push(cx.observe(&this.user_store, |this, _, cx| {
                    this.update_entries(true, cx)
                }));
            this.subscriptions
                .push(cx.observe(&this.channel_store, move |this, _, cx| {
                    this.update_entries(true, cx)
                }));
            this.subscriptions
                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
            this.subscriptions.push(cx.subscribe_in(
                &this.channel_store,
                window,
                |this, _channel_store, e, window, cx| match e {
                    ChannelEvent::ChannelCreated(channel_id)
                    | ChannelEvent::ChannelRenamed(channel_id) => {
                        if this.take_editing_state(window, cx) {
                            this.update_entries(false, cx);
                            this.selection = this.entries.iter().position(|entry| {
                                if let ListEntry::Channel { channel, .. } = entry {
                                    channel.id == *channel_id
                                } else {
                                    false
                                }
                            });
                        }
                    }
                },
            ));
            this.subscriptions.push(cx.subscribe_in(
                &this.notification_store,
                window,
                Self::on_notification_event,
            ));

            this
        })
    }

    pub async fn load(
        workspace: WeakEntity<Workspace>,
        mut cx: AsyncWindowContext,
    ) -> anyhow::Result<Entity<Self>> {
        let serialized_panel = match workspace
            .read_with(&cx, |workspace, _| {
                CollabPanel::serialization_key(workspace)
            })
            .ok()
            .flatten()
        {
            Some(serialization_key) => {
                let kvp = cx.update(|_, cx| KeyValueStore::global(cx))?;
                kvp.read_kvp(&serialization_key)
                    .context("reading collaboration panel from key value store")
                    .log_err()
                    .flatten()
                    .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
                    .transpose()
                    .log_err()
                    .flatten()
            }
            None => None,
        };

        workspace.update_in(&mut cx, |workspace, window, cx| {
            let panel = CollabPanel::new(workspace, window, cx);
            if let Some(serialized_panel) = serialized_panel {
                panel.update(cx, |panel, cx| {
                    panel.collapsed_channels = serialized_panel
                        .collapsed_channels
                        .unwrap_or_default()
                        .iter()
                        .map(|cid| ChannelId(*cid))
                        .collect();
                    cx.notify();
                });
            }

            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_KEY)
                .ok()
                .flatten()
                .and_then(|json| serde_json::from_str::<Vec<u64>>(&json).ok())
                .unwrap_or_default()
                .into_iter()
                .map(ChannelId)
                .collect();

            if !favorites.is_empty() {
                panel.update(cx, |panel, cx| {
                    panel.channel_store.update(cx, |store, cx| {
                        store.set_favorite_channel_ids(favorites, cx);
                    });
                });
            }

            panel
        })
    }

    fn serialization_key(workspace: &Workspace) -> Option<String> {
        workspace
            .database_id()
            .map(|id| i64::from(id).to_string())
            .or(workspace.session_id())
            .map(|id| format!("{}-{:?}", COLLABORATION_PANEL_KEY, id))
    }

    fn serialize(&mut self, cx: &mut Context<Self>) {
        let Some(serialization_key) = self
            .workspace
            .read_with(cx, |workspace, _| CollabPanel::serialization_key(workspace))
            .ok()
            .flatten()
        else {
            return;
        };
        let collapsed_channels = if self.collapsed_channels.is_empty() {
            None
        } else {
            Some(self.collapsed_channels.iter().map(|id| id.0).collect())
        };

        let kvp = KeyValueStore::global(cx);
        self.pending_panel_serialization = cx.background_spawn(
            async move {
                kvp.write_kvp(
                    serialization_key,
                    serde_json::to_string(&SerializedCollabPanel { collapsed_channels })?,
                )
                .await?;
                anyhow::Ok(())
            }
            .log_err(),
        );
    }

    fn scroll_to_item(&mut self, ix: usize) {
        self.list_state.scroll_to_reveal_item(ix)
    }

    fn update_entries(&mut self, select_same_item: bool, cx: &mut Context<Self>) {
        let query = self.filter_editor.read(cx).text(cx);
        let fg_executor = cx.foreground_executor().clone();
        let executor = cx.background_executor().clone();

        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
        let old_entries = mem::take(&mut self.entries);
        let mut scroll_to_top = false;

        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
            self.entries.push(ListEntry::Header(Section::ActiveCall));
            if !old_entries
                .iter()
                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
            {
                scroll_to_top = true;
            }

            if !self.collapsed_sections.contains(&Section::ActiveCall) {
                let room = room.read(cx);

                if query.is_empty()
                    && let Some(channel_id) = room.channel_id()
                {
                    self.entries.push(ListEntry::ChannelNotes { channel_id });
                }

                // Populate the active user.
                if let Some(user) = self.user_store.read(cx).current_user() {
                    self.match_candidates.clear();
                    self.match_candidates
                        .push(StringMatchCandidate::new(0, &user.github_login));
                    let matches = fg_executor.block_on(match_strings(
                        &self.match_candidates,
                        &query,
                        true,
                        true,
                        usize::MAX,
                        &Default::default(),
                        executor.clone(),
                    ));
                    if !matches.is_empty() {
                        let user_id = user.legacy_id;
                        self.entries.push(ListEntry::CallParticipant {
                            user,
                            peer_id: None,
                            is_pending: false,
                            role: room.local_participant().role,
                        });
                        let mut projects = room.local_participant().projects.iter().peekable();
                        while let Some(project) = projects.next() {
                            self.entries.push(ListEntry::ParticipantProject {
                                project_id: project.id,
                                worktree_root_names: project.worktree_root_names.clone(),
                                host_user_id: user_id,
                                is_last: projects.peek().is_none() && !room.is_sharing_screen(),
                            });
                        }
                        if room.is_sharing_screen() {
                            self.entries.push(ListEntry::ParticipantScreen {
                                peer_id: None,
                                is_last: true,
                            });
                        }
                    }
                }

                // Populate remote participants.
                self.match_candidates.clear();
                self.match_candidates
                    .extend(room.remote_participants().values().map(|participant| {
                        StringMatchCandidate::new(
                            participant.user.legacy_id as usize,
                            &participant.user.github_login,
                        )
                    }));
                let mut matches = fg_executor.block_on(match_strings(
                    &self.match_candidates,
                    &query,
                    true,
                    true,
                    usize::MAX,
                    &Default::default(),
                    executor.clone(),
                ));
                matches.sort_by(|a, b| {
                    let a_is_guest = room.role_for_user(a.candidate_id as u64)
                        == Some(proto::ChannelRole::Guest);
                    let b_is_guest = room.role_for_user(b.candidate_id as u64)
                        == Some(proto::ChannelRole::Guest);
                    a_is_guest
                        .cmp(&b_is_guest)
                        .then_with(|| a.string.cmp(&b.string))
                });
                for mat in matches {
                    let user_id = mat.candidate_id as u64;
                    let participant = &room.remote_participants()[&user_id];
                    self.entries.push(ListEntry::CallParticipant {
                        user: participant.user.clone(),
                        peer_id: Some(participant.peer_id),
                        is_pending: false,
                        role: participant.role,
                    });
                    let mut projects = participant.projects.iter().peekable();
                    while let Some(project) = projects.next() {
                        self.entries.push(ListEntry::ParticipantProject {
                            project_id: project.id,
                            worktree_root_names: project.worktree_root_names.clone(),
                            host_user_id: participant.user.legacy_id,
                            is_last: projects.peek().is_none() && !participant.has_video_tracks(),
                        });
                    }
                    if participant.has_video_tracks() {
                        self.entries.push(ListEntry::ParticipantScreen {
                            peer_id: Some(participant.peer_id),
                            is_last: true,
                        });
                    }
                }

                // Populate pending participants.
                self.match_candidates.clear();
                self.match_candidates
                    .extend(room.pending_participants().iter().enumerate().map(
                        |(id, participant)| {
                            StringMatchCandidate::new(id, &participant.github_login)
                        },
                    ));
                let matches = fg_executor.block_on(match_strings(
                    &self.match_candidates,
                    &query,
                    true,
                    true,
                    usize::MAX,
                    &Default::default(),
                    executor.clone(),
                ));
                self.entries
                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
                        user: room.pending_participants()[mat.candidate_id].clone(),
                        peer_id: None,
                        is_pending: true,
                        role: proto::ChannelRole::Member,
                    }));
            }
        }

        let mut request_entries = Vec::new();

        let channel_store = self.channel_store.read(cx);
        let user_store = self.user_store.read(cx);

        let favorite_ids = channel_store.favorite_channel_ids();
        if !favorite_ids.is_empty() {
            let favorite_channels: Vec<_> = favorite_ids
                .iter()
                .filter_map(|id| channel_store.channel_for_id(*id))
                .collect();

            self.match_candidates.clear();
            self.match_candidates.extend(
                favorite_channels
                    .iter()
                    .enumerate()
                    .map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
            );

            let matches = fg_executor.block_on(match_strings(
                &self.match_candidates,
                &query,
                true,
                true,
                usize::MAX,
                &Default::default(),
                executor.clone(),
            ));

            if !matches.is_empty() || query.is_empty() {
                self.entries
                    .push(ListEntry::Header(Section::FavoriteChannels));

                let matches_by_candidate: HashMap<usize, &StringMatch> =
                    matches.iter().map(|mat| (mat.candidate_id, mat)).collect();

                for (ix, channel) in favorite_channels.iter().enumerate() {
                    if !query.is_empty() && !matches_by_candidate.contains_key(&ix) {
                        continue;
                    }
                    self.entries.push(ListEntry::Channel {
                        channel: (*channel).clone(),
                        depth: 0,
                        has_children: false,
                        is_favorite: true,
                        string_match: matches_by_candidate.get(&ix).cloned().cloned(),
                    });
                }
            }
        }

        self.entries.push(ListEntry::Header(Section::Channels));

        if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
            self.match_candidates.clear();
            self.match_candidates.extend(
                channel_store
                    .ordered_channels()
                    .enumerate()
                    .map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
            );
            let mut channels = channel_store
                .ordered_channels()
                .map(|(_, chan)| chan)
                .collect::<Vec<_>>();
            let matches = fg_executor.block_on(match_strings(
                &self.match_candidates,
                &query,
                true,
                true,
                usize::MAX,
                &Default::default(),
                executor.clone(),
            ));

            let matches_by_id: HashMap<_, _> = matches
                .iter()
                .map(|mat| (channels[mat.candidate_id].id, mat.clone()))
                .collect();

            let channel_ids_of_matches_or_parents: HashSet<_> = matches
                .iter()
                .flat_map(|mat| {
                    let match_channel = channels[mat.candidate_id];

                    match_channel
                        .parent_path
                        .iter()
                        .copied()
                        .chain(Some(match_channel.id))
                })
                .collect();

            channels.retain(|chan| channel_ids_of_matches_or_parents.contains(&chan.id));

            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| occupied_channel_ids_or_ancestors.contains(&channel.id));
            }

            if let Some(state) = &self.channel_editing_state
                && matches!(state, ChannelEditingState::Create { location: None, .. })
            {
                self.entries.push(ListEntry::ChannelEditor { depth: 0 });
            }

            let should_respect_collapse = query.is_empty() && !self.filter_occupied_channels;
            let mut collapse_depth = None;

            for (idx, channel) in channels.into_iter().enumerate() {
                let depth = channel.parent_path.len();

                if should_respect_collapse {
                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
                        collapse_depth = Some(depth);
                    } else if let Some(collapsed_depth) = collapse_depth {
                        if depth > collapsed_depth {
                            continue;
                        }
                        if self.is_channel_collapsed(channel.id) {
                            collapse_depth = Some(depth);
                        } else {
                            collapse_depth = None;
                        }
                    }
                }

                let has_children = channel_store
                    .channel_at_index(idx + 1)
                    .is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id]));

                match &self.channel_editing_state {
                    Some(ChannelEditingState::Create {
                        location: parent_id,
                        ..
                    }) if *parent_id == Some(channel.id) => {
                        self.entries.push(ListEntry::Channel {
                            channel: channel.clone(),
                            depth,
                            has_children: false,
                            is_favorite: false,
                            string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
                        });
                        self.entries
                            .push(ListEntry::ChannelEditor { depth: depth + 1 });
                    }
                    Some(ChannelEditingState::Rename {
                        location: parent_id,
                        ..
                    }) if parent_id == &channel.id => {
                        self.entries.push(ListEntry::ChannelEditor { depth });
                    }
                    _ => {
                        self.entries.push(ListEntry::Channel {
                            channel: channel.clone(),
                            depth,
                            has_children,
                            is_favorite: false,
                            string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
                        });
                    }
                }
            }
        }

        let channel_invites = channel_store.channel_invitations();
        if !channel_invites.is_empty() {
            self.match_candidates.clear();
            self.match_candidates.extend(
                channel_invites
                    .iter()
                    .enumerate()
                    .map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
            );
            let matches = fg_executor.block_on(match_strings(
                &self.match_candidates,
                &query,
                true,
                true,
                usize::MAX,
                &Default::default(),
                executor.clone(),
            ));
            request_entries.extend(
                matches
                    .iter()
                    .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())),
            );

            if !request_entries.is_empty() {
                self.entries
                    .push(ListEntry::Header(Section::ChannelInvites));
                if !self.collapsed_sections.contains(&Section::ChannelInvites) {
                    self.entries.append(&mut request_entries);
                }
            }
        }

        self.entries.push(ListEntry::Header(Section::Contacts));

        request_entries.clear();
        let incoming = user_store.incoming_contact_requests();
        if !incoming.is_empty() {
            self.match_candidates.clear();
            self.match_candidates.extend(
                incoming
                    .iter()
                    .enumerate()
                    .map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
            );
            let matches = fg_executor.block_on(match_strings(
                &self.match_candidates,
                &query,
                true,
                true,
                usize::MAX,
                &Default::default(),
                executor.clone(),
            ));
            request_entries.extend(
                matches
                    .iter()
                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
            );
        }

        let outgoing = user_store.outgoing_contact_requests();
        if !outgoing.is_empty() {
            self.match_candidates.clear();
            self.match_candidates.extend(
                outgoing
                    .iter()
                    .enumerate()
                    .map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
            );
            let matches = fg_executor.block_on(match_strings(
                &self.match_candidates,
                &query,
                true,
                true,
                usize::MAX,
                &Default::default(),
                executor.clone(),
            ));
            request_entries.extend(
                matches
                    .iter()
                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
            );
        }

        if !request_entries.is_empty() {
            self.entries
                .push(ListEntry::Header(Section::ContactRequests));
            if !self.collapsed_sections.contains(&Section::ContactRequests) {
                self.entries.append(&mut request_entries);
            }
        }

        let contacts = user_store.contacts();
        if !contacts.is_empty() {
            self.match_candidates.clear();
            self.match_candidates.extend(
                contacts
                    .iter()
                    .enumerate()
                    .map(|(ix, contact)| StringMatchCandidate::new(ix, &contact.user.github_login)),
            );

            let matches = fg_executor.block_on(match_strings(
                &self.match_candidates,
                &query,
                true,
                true,
                usize::MAX,
                &Default::default(),
                executor,
            ));

            let (online_contacts, offline_contacts) = matches
                .iter()
                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);

            for (matches, section) in [
                (online_contacts, Section::Online),
                (offline_contacts, Section::Offline),
            ] {
                if !matches.is_empty() {
                    self.entries.push(ListEntry::Header(section));
                    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(ListEntry::Contact {
                                contact: contact.clone(),
                                calling: active_call
                                    .pending_invites()
                                    .contains(&contact.user.legacy_id),
                            });
                        }
                    }
                }
            }
        }

        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
            self.entries.push(ListEntry::ContactPlaceholder);
        }

        if select_same_item {
            if let Some(prev_selected_entry) = prev_selected_entry {
                let prev_selection = self.selection.take();
                for (ix, entry) in self.entries.iter().enumerate() {
                    if *entry == prev_selected_entry {
                        self.selection = Some(ix);
                        break;
                    }
                }
                if self.selection.is_none() {
                    self.selection = prev_selection.and_then(|prev_ix| {
                        if self.entries.is_empty() {
                            None
                        } else {
                            Some(prev_ix.min(self.entries.len() - 1))
                        }
                    });
                }
            }
        } else {
            self.selection = self.selection.and_then(|prev_selection| {
                if self.entries.is_empty() {
                    None
                } else {
                    Some(prev_selection.min(self.entries.len() - 1))
                }
            });
        }

        let old_scroll_top = self.list_state.logical_scroll_top();
        self.list_state.reset(self.entries.len());

        if scroll_to_top {
            self.list_state.scroll_to(ListOffset::default());
        } else {
            // Attempt to maintain the same scroll position.
            if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
                let new_scroll_top = self
                    .entries
                    .iter()
                    .position(|entry| entry == old_top_entry)
                    .map(|item_ix| ListOffset {
                        item_ix,
                        offset_in_item: old_scroll_top.offset_in_item,
                    })
                    .or_else(|| {
                        let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
                        let item_ix = self
                            .entries
                            .iter()
                            .position(|entry| entry == entry_after_old_top)?;
                        Some(ListOffset {
                            item_ix,
                            offset_in_item: Pixels::ZERO,
                        })
                    })
                    .or_else(|| {
                        let entry_before_old_top =
                            old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
                        let item_ix = self
                            .entries
                            .iter()
                            .position(|entry| entry == entry_before_old_top)?;
                        Some(ListOffset {
                            item_ix,
                            offset_in_item: Pixels::ZERO,
                        })
                    });

                self.list_state
                    .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
            }
        }

        cx.notify();
    }

    fn render_call_participant(
        &self,
        user: &Arc<User>,
        peer_id: Option<PeerId>,
        is_pending: bool,
        role: proto::ChannelRole,
        is_selected: bool,
        cx: &mut Context<Self>,
    ) -> ListItem {
        let user_id = user.legacy_id;
        let is_current_user = self
            .user_store
            .read(cx)
            .current_user()
            .map(|user| user.legacy_id)
            == Some(user_id);
        let tooltip = format!("Follow {}", user.github_login);

        let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
            room.read(cx).local_participant().role == proto::ChannelRole::Admin
        });

        let end_slot = if is_pending {
            Label::new("Calling").color(Color::Muted).into_any_element()
        } else if is_current_user {
            IconButton::new("leave-call", IconName::Exit)
                .icon_size(IconSize::Small)
                .tooltip(Tooltip::text("Leave Call"))
                .on_click(move |_, window, cx| Self::leave_call(window, cx))
                .into_any_element()
        } else if role == proto::ChannelRole::Guest {
            Label::new("Guest").color(Color::Muted).into_any_element()
        } else if role == proto::ChannelRole::Talker {
            Label::new("Mic only")
                .color(Color::Muted)
                .into_any_element()
        } else {
            Empty.into_any_element()
        };

        ListItem::new(user.github_login.clone())
            .start_slot(Avatar::new(user.avatar_uri.clone()))
            .child(render_participant_name_and_handle(user))
            .toggle_state(is_selected)
            .end_slot(end_slot)
            .tooltip(Tooltip::text("Click to Follow"))
            .when_some(peer_id, |el, peer_id| {
                if role == proto::ChannelRole::Guest {
                    return el;
                }
                el.tooltip(Tooltip::text(tooltip.clone()))
                    .on_click(cx.listener(move |this, _, window, cx| {
                        this.workspace
                            .update(cx, |workspace, cx| workspace.follow(peer_id, window, cx))
                            .ok();
                    }))
            })
            .when(is_call_admin, |el| {
                el.on_secondary_mouse_down(cx.listener(
                    move |this, event: &MouseDownEvent, window, cx| {
                        this.deploy_participant_context_menu(
                            event.position,
                            user_id,
                            role,
                            window,
                            cx,
                        )
                    },
                ))
            })
    }

    fn render_participant_project(
        &self,
        project_id: u64,
        worktree_root_names: &[String],
        host_user_id: u64,
        is_last: bool,
        is_selected: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let project_name: SharedString = if worktree_root_names.is_empty() {
            "untitled".to_string()
        } else {
            worktree_root_names.join(", ")
        }
        .into();

        ListItem::new(project_id as usize)
            .height(rems_from_px(24.))
            .toggle_state(is_selected)
            .on_click(cx.listener(move |this, _, window, cx| {
                this.workspace
                    .update(cx, |workspace, cx| {
                        let app_state = workspace.app_state().clone();
                        workspace::join_in_room_project(project_id, host_user_id, app_state, cx)
                            .detach_and_prompt_err(
                                "Failed to join project",
                                window,
                                cx,
                                |error, _, _| Some(format!("{error:#}")),
                            );
                    })
                    .ok();
            }))
            .start_slot(
                h_flex()
                    .gap_1p5()
                    .child(render_tree_branch(is_last, false, window, cx))
                    .child(
                        Icon::new(IconName::Folder)
                            .size(IconSize::Small)
                            .color(Color::Muted),
                    ),
            )
            .child(Label::new(project_name.clone()))
            .tooltip(Tooltip::text(format!("Open {}", project_name)))
    }

    fn render_participant_screen(
        &self,
        peer_id: Option<PeerId>,
        is_last: bool,
        is_selected: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);

        ListItem::new(("screen", id))
            .height(rems_from_px(24.))
            .toggle_state(is_selected)
            .start_slot(
                h_flex()
                    .gap_1p5()
                    .child(render_tree_branch(is_last, false, window, cx))
                    .child(
                        Icon::new(IconName::Screen)
                            .size(IconSize::Small)
                            .color(Color::Muted),
                    ),
            )
            .child(Label::new("Screen"))
            .when_some(peer_id, |this, _| {
                this.on_click(cx.listener(move |this, _, window, cx| {
                    this.workspace
                        .update(cx, |workspace, cx| {
                            workspace.open_shared_screen(peer_id.unwrap(), window, cx)
                        })
                        .ok();
                }))
                .tooltip(Tooltip::text("Open Shared Screen"))
            })
    }

    fn take_editing_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
        if self.channel_editing_state.take().is_some() {
            self.channel_name_editor.update(cx, |editor, cx| {
                editor.set_text("", window, cx);
            });
            true
        } else {
            false
        }
    }

    fn render_channel_notes(
        &self,
        channel_id: ChannelId,
        is_selected: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let channel_store = self.channel_store.read(cx);
        let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);

        ListItem::new("channel-notes")
            .height(rems_from_px(24.))
            .toggle_state(is_selected)
            .on_click(cx.listener(move |this, _, window, cx| {
                this.open_channel_notes(channel_id, window, cx);
            }))
            .start_slot(
                h_flex()
                    .relative()
                    .gap_1p5()
                    .child(render_tree_branch(false, true, window, cx))
                    .child(
                        h_flex()
                            .child(
                                Icon::new(IconName::Reader)
                                    .size(IconSize::Small)
                                    .color(Color::Muted),
                            )
                            .when(has_channel_buffer_changed, |this| {
                                this.child(
                                    div()
                                        .absolute()
                                        .top_neg_0p5()
                                        .right_0()
                                        .child(Indicator::dot().color(Color::Info)),
                                )
                            }),
                    ),
            )
            .child(Label::new("notes"))
            .tooltip(Tooltip::text("Open Channel Notes"))
    }

    fn has_subchannels(&self, ix: usize) -> bool {
        self.entries.get(ix).is_some_and(|entry| {
            if let ListEntry::Channel { has_children, .. } = entry {
                *has_children
            } else {
                false
            }
        })
    }

    fn deploy_participant_context_menu(
        &mut self,
        position: Point<Pixels>,
        user_id: u64,
        role: proto::ChannelRole,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let this = cx.entity();
        if !(role == proto::ChannelRole::Guest
            || role == proto::ChannelRole::Talker
            || role == proto::ChannelRole::Member)
        {
            return;
        }

        let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, _| {
            if role == proto::ChannelRole::Guest {
                context_menu = context_menu.entry(
                    "Grant Mic Access",
                    None,
                    window.handler_for(&this, move |_, window, cx| {
                        ActiveCall::global(cx)
                            .update(cx, |call, cx| {
                                let Some(room) = call.room() else {
                                    return Task::ready(Ok(()));
                                };
                                room.update(cx, |room, cx| {
                                    room.set_participant_role(
                                        user_id,
                                        proto::ChannelRole::Talker,
                                        cx,
                                    )
                                })
                            })
                            .detach_and_prompt_err(
                                "Failed to grant mic access",
                                window,
                                cx,
                                |_, _, _| None,
                            )
                    }),
                );
            }
            if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker {
                context_menu = context_menu.entry(
                    "Grant Write Access",
                    None,
                    window.handler_for(&this, move |_, window, cx| {
                        ActiveCall::global(cx)
                            .update(cx, |call, cx| {
                                let Some(room) = call.room() else {
                                    return Task::ready(Ok(()));
                                };
                                room.update(cx, |room, cx| {
                                    room.set_participant_role(
                                        user_id,
                                        proto::ChannelRole::Member,
                                        cx,
                                    )
                                })
                            })
                            .detach_and_prompt_err("Failed to grant write access", window, cx, |e, _, _| {
                                match e.error_code() {
                                    ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()),
                                    _ => None,
                                }
                            })
                    }),
                );
            }
            if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker {
                let label = if role == proto::ChannelRole::Talker {
                    "Mute"
                } else {
                    "Revoke Access"
                };
                context_menu = context_menu.entry(
                    label,
                    None,
                    window.handler_for(&this, move |_, window, cx| {
                        ActiveCall::global(cx)
                            .update(cx, |call, cx| {
                                let Some(room) = call.room() else {
                                    return Task::ready(Ok(()));
                                };
                                room.update(cx, |room, cx| {
                                    room.set_participant_role(
                                        user_id,
                                        proto::ChannelRole::Guest,
                                        cx,
                                    )
                                })
                            })
                            .detach_and_prompt_err(
                                "Failed to revoke access",
                                window,
                                cx,
                                |_, _, _| None,
                            )
                    }),
                );
            }

            context_menu
        });

        window.focus(&context_menu.focus_handle(cx), cx);
        let subscription = cx.subscribe_in(
            &context_menu,
            window,
            |this, _, _: &DismissEvent, window, cx| {
                if this.context_menu.as_ref().is_some_and(|context_menu| {
                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
                }) {
                    cx.focus_self(window);
                }
                this.context_menu.take();
                cx.notify();
            },
        );
        self.context_menu = Some((context_menu, position, subscription));
    }

    fn deploy_channel_context_menu(
        &mut self,
        position: Point<Pixels>,
        channel_id: ChannelId,
        ix: usize,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
            self.channel_store
                .read(cx)
                .channel_for_id(clipboard.channel_id)
                .map(|channel| channel.name.clone())
        });
        let this = cx.entity();

        let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, cx| {
            if self.has_subchannels(ix) {
                let expand_action_name = if self.is_channel_collapsed(channel_id) {
                    "Expand Subchannels"
                } else {
                    "Collapse Subchannels"
                };
                context_menu = context_menu.entry(
                    expand_action_name,
                    None,
                    window.handler_for(&this, move |this, window, cx| {
                        this.toggle_channel_collapsed(channel_id, window, cx)
                    }),
                );
            }

            context_menu = context_menu
                .entry(
                    "Open Notes",
                    None,
                    window.handler_for(&this, move |this, window, cx| {
                        this.open_channel_notes(channel_id, window, cx)
                    }),
                )
                .entry(
                    "Copy Channel Link",
                    None,
                    window.handler_for(&this, move |this, _, cx| {
                        this.copy_channel_link(channel_id, cx)
                    }),
                )
                .entry(
                    "Copy Channel Notes Link",
                    None,
                    window.handler_for(&this, move |this, _, cx| {
                        this.copy_channel_notes_link(channel_id, cx)
                    }),
                )
                .separator()
                .entry(
                    if self.is_channel_favorited(channel_id, cx) {
                        "Remove from Favorites"
                    } else {
                        "Add to Favorites"
                    },
                    None,
                    window.handler_for(&this, move |this, _window, cx| {
                        this.toggle_favorite_channel(channel_id, cx)
                    }),
                );

            let mut has_destructive_actions = false;
            if self.channel_store.read(cx).is_channel_admin(channel_id) {
                has_destructive_actions = true;
                context_menu = context_menu
                    .separator()
                    .entry(
                        "New Subchannel",
                        None,
                        window.handler_for(&this, move |this, window, cx| {
                            this.new_subchannel(channel_id, window, cx)
                        }),
                    )
                    .entry(
                        "Rename",
                        Some(Box::new(SecondaryConfirm)),
                        window.handler_for(&this, move |this, window, cx| {
                            this.rename_channel(channel_id, window, cx)
                        }),
                    );

                if let Some(channel_name) = clipboard_channel_name {
                    context_menu = context_menu.separator().entry(
                        format!("Move '#{}' here", channel_name),
                        None,
                        window.handler_for(&this, move |this, window, cx| {
                            this.move_channel_on_clipboard(channel_id, window, cx)
                        }),
                    );
                }

                if self.channel_store.read(cx).is_root_channel(channel_id) {
                    context_menu = context_menu.separator().entry(
                        "Manage Members",
                        None,
                        window.handler_for(&this, move |this, window, cx| {
                            this.manage_members(channel_id, window, cx)
                        }),
                    )
                } else {
                    context_menu = context_menu.entry(
                        "Move this channel",
                        None,
                        window.handler_for(&this, move |this, window, cx| {
                            this.start_move_channel(channel_id, window, cx)
                        }),
                    );
                    if self.channel_store.read(cx).is_public_channel(channel_id) {
                        context_menu = context_menu.separator().entry(
                            "Make Channel Private",
                            None,
                            window.handler_for(&this, move |this, window, cx| {
                                this.set_channel_visibility(
                                    channel_id,
                                    ChannelVisibility::Members,
                                    window,
                                    cx,
                                )
                            }),
                        )
                    } else {
                        context_menu = context_menu.separator().entry(
                            "Make Channel Public",
                            None,
                            window.handler_for(&this, move |this, window, cx| {
                                this.set_channel_visibility(
                                    channel_id,
                                    ChannelVisibility::Public,
                                    window,
                                    cx,
                                )
                            }),
                        )
                    }
                }

                context_menu = context_menu.entry(
                    "Delete",
                    None,
                    window.handler_for(&this, move |this, window, cx| {
                        this.remove_channel(channel_id, window, cx)
                    }),
                );
            }

            if self.channel_store.read(cx).is_root_channel(channel_id) {
                if !has_destructive_actions {
                    context_menu = context_menu.separator()
                }
                context_menu = context_menu.entry(
                    "Leave Channel",
                    None,
                    window.handler_for(&this, move |this, window, cx| {
                        this.leave_channel(channel_id, window, cx)
                    }),
                );
            }

            context_menu
        });

        window.focus(&context_menu.focus_handle(cx), cx);
        let subscription = cx.subscribe_in(
            &context_menu,
            window,
            |this, _, _: &DismissEvent, window, cx| {
                if this.context_menu.as_ref().is_some_and(|context_menu| {
                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
                }) {
                    cx.focus_self(window);
                }
                this.context_menu.take();
                cx.notify();
            },
        );
        self.context_menu = Some((context_menu, position, subscription));

        cx.notify();
    }

    fn deploy_contact_context_menu(
        &mut self,
        position: Point<Pixels>,
        contact: Arc<Contact>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let this = cx.entity();
        let in_room = ActiveCall::global(cx).read(cx).room().is_some();

        let context_menu = ContextMenu::build(window, cx, |mut context_menu, _, _| {
            let user_id = contact.user.legacy_id;

            if contact.online && !contact.busy {
                let label = if in_room {
                    format!("Invite {} to join", contact.user.github_login)
                } else {
                    format!("Call {}", contact.user.github_login)
                };
                context_menu = context_menu.entry(label, None, {
                    let this = this.clone();
                    move |window, cx| {
                        this.update(cx, |this, cx| {
                            this.call(user_id, window, cx);
                        });
                    }
                });
            }

            context_menu.entry("Remove Contact", None, {
                let this = this.clone();
                move |window, cx| {
                    this.update(cx, |this, cx| {
                        this.remove_contact(
                            contact.user.legacy_id,
                            &contact.user.github_login,
                            window,
                            cx,
                        );
                    });
                }
            })
        });

        window.focus(&context_menu.focus_handle(cx), cx);
        let subscription = cx.subscribe_in(
            &context_menu,
            window,
            |this, _, _: &DismissEvent, window, cx| {
                if this.context_menu.as_ref().is_some_and(|context_menu| {
                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
                }) {
                    cx.focus_self(window);
                }
                this.context_menu.take();
                cx.notify();
            },
        );
        self.context_menu = Some((context_menu, position, subscription));

        cx.notify();
    }

    fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
        self.filter_editor.update(cx, |editor, cx| {
            if editor.buffer().read(cx).len(cx).0 > 0 {
                editor.set_text("", window, cx);
                true
            } else {
                false
            }
        })
    }

    fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
        if cx.stop_active_drag(window) {
            return;
        } else if self.take_editing_state(window, cx) {
            window.focus(&self.filter_editor.focus_handle(cx), cx);
        } else if !self.reset_filter_editor_text(window, cx) {
            self.focus_handle.focus(window, cx);
        }

        if self.context_menu.is_some() {
            self.context_menu.take();
            cx.notify();
        }

        self.update_entries(false, cx);
    }

    pub fn select_next(&mut self, _: &SelectNext, _: &mut Window, cx: &mut Context<Self>) {
        let ix = self.selection.map_or(0, |ix| ix + 1);
        if ix < self.entries.len() {
            self.selection = Some(ix);
        }

        if let Some(ix) = self.selection {
            self.scroll_to_item(ix)
        }
        cx.notify();
    }

    pub fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
        let ix = self.selection.take().unwrap_or(0);
        if ix > 0 {
            self.selection = Some(ix - 1);
        }

        if let Some(ix) = self.selection {
            self.scroll_to_item(ix)
        }
        cx.notify();
    }

    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
        if self.confirm_channel_edit(window, cx) {
            return;
        }

        if let Some(selection) = self.selection
            && let Some(entry) = self.entries.get(selection)
        {
            match entry {
                ListEntry::Header(section) => match section {
                    Section::ActiveCall => Self::leave_call(window, cx),
                    Section::Channels => self.new_root_channel(window, cx),
                    Section::Contacts => self.toggle_contact_finder(window, cx),
                    Section::FavoriteChannels
                    | Section::ContactRequests
                    | Section::Online
                    | Section::Offline
                    | Section::ChannelInvites => {
                        self.toggle_section_expanded(*section, cx);
                    }
                },
                ListEntry::Contact { contact, calling } => {
                    if contact.online && !contact.busy && !calling {
                        self.call(contact.user.legacy_id, window, cx);
                    }
                }
                ListEntry::ParticipantProject {
                    project_id,
                    host_user_id,
                    ..
                } => {
                    if let Some(workspace) = self.workspace.upgrade() {
                        let app_state = workspace.read(cx).app_state().clone();
                        workspace::join_in_room_project(*project_id, *host_user_id, app_state, cx)
                            .detach_and_prompt_err(
                                "Failed to join project",
                                window,
                                cx,
                                |error, _, _| Some(format!("{error:#}")),
                            );
                    }
                }
                ListEntry::ParticipantScreen { peer_id, .. } => {
                    let Some(peer_id) = peer_id else {
                        return;
                    };
                    if let Some(workspace) = self.workspace.upgrade() {
                        workspace.update(cx, |workspace, cx| {
                            workspace.open_shared_screen(*peer_id, window, cx)
                        });
                    }
                }
                ListEntry::Channel { channel, .. } => {
                    let is_active = maybe!({
                        let call_channel = ActiveCall::global(cx)
                            .read(cx)
                            .room()?
                            .read(cx)
                            .channel_id()?;

                        Some(call_channel == channel.id)
                    })
                    .unwrap_or(false);
                    if is_active {
                        self.open_channel_notes(channel.id, window, cx)
                    } else {
                        self.join_channel(channel.id, window, cx)
                    }
                }
                ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx),
                ListEntry::CallParticipant { user, peer_id, .. } => {
                    if Some(user) == self.user_store.read(cx).current_user().as_ref() {
                        Self::leave_call(window, cx);
                    } else if let Some(peer_id) = peer_id {
                        self.workspace
                            .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx))
                            .ok();
                    }
                }
                ListEntry::IncomingRequest(user) => {
                    self.respond_to_contact_request(user.legacy_id, true, window, cx)
                }
                ListEntry::ChannelInvite(channel) => {
                    self.respond_to_channel_invite(channel.id, true, cx)
                }
                ListEntry::ChannelNotes { channel_id } => {
                    self.open_channel_notes(*channel_id, window, cx)
                }
                ListEntry::OutgoingRequest(_) => {}
                ListEntry::ChannelEditor { .. } => {}
            }
        }
    }

    fn insert_space(&mut self, _: &InsertSpace, window: &mut Window, cx: &mut Context<Self>) {
        if self.channel_editing_state.is_some() {
            self.channel_name_editor.update(cx, |editor, cx| {
                editor.insert(" ", window, cx);
            });
        } else if self.filter_editor.focus_handle(cx).is_focused(window) {
            self.filter_editor.update(cx, |editor, cx| {
                editor.insert(" ", window, cx);
            });
        }
    }

    fn confirm_channel_edit(&mut self, window: &mut Window, cx: &mut Context<CollabPanel>) -> bool {
        if let Some(editing_state) = &mut self.channel_editing_state {
            match editing_state {
                ChannelEditingState::Create {
                    location,
                    pending_name,
                    ..
                } => {
                    if pending_name.is_some() {
                        return false;
                    }
                    let channel_name = self.channel_name_editor.read(cx).text(cx);

                    *pending_name = Some(channel_name.clone());

                    let create = self.channel_store.update(cx, |channel_store, cx| {
                        channel_store.create_channel(&channel_name, *location, cx)
                    });
                    if location.is_none() {
                        cx.spawn_in(window, async move |this, cx| {
                            let channel_id = create.await?;
                            this.update_in(cx, |this, window, cx| {
                                this.show_channel_modal(
                                    channel_id,
                                    channel_modal::Mode::InviteMembers,
                                    window,
                                    cx,
                                )
                            })
                        })
                        .detach_and_prompt_err(
                            "Failed to create channel",
                            window,
                            cx,
                            |_, _, _| None,
                        );
                    } else {
                        create.detach_and_prompt_err(
                            "Failed to create channel",
                            window,
                            cx,
                            |_, _, _| None,
                        );
                    }
                    cx.notify();
                }
                ChannelEditingState::Rename {
                    location,
                    pending_name,
                } => {
                    if pending_name.is_some() {
                        return false;
                    }
                    let channel_name = self.channel_name_editor.read(cx).text(cx);
                    *pending_name = Some(channel_name.clone());

                    self.channel_store
                        .update(cx, |channel_store, cx| {
                            channel_store.rename(*location, &channel_name, cx)
                        })
                        .detach();
                    cx.notify();
                }
            }
            cx.focus_self(window);
            true
        } else {
            false
        }
    }

    fn toggle_section_expanded(&mut self, section: Section, cx: &mut Context<Self>) {
        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
            self.collapsed_sections.remove(ix);
        } else {
            self.collapsed_sections.push(section);
        }
        self.update_entries(false, cx);
    }

    fn collapse_selected_channel(
        &mut self,
        _: &CollapseSelectedChannel,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
            return;
        };

        if self.is_channel_collapsed(channel_id) {
            return;
        }

        self.toggle_channel_collapsed(channel_id, window, cx);
    }

    fn expand_selected_channel(
        &mut self,
        _: &ExpandSelectedChannel,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
            return;
        };

        if !self.is_channel_collapsed(id) {
            return;
        }

        self.toggle_channel_collapsed(id, window, cx)
    }

    fn toggle_channel_collapsed(
        &mut self,
        channel_id: ChannelId,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        match self.collapsed_channels.binary_search(&channel_id) {
            Ok(ix) => {
                self.collapsed_channels.remove(ix);
            }
            Err(ix) => {
                self.collapsed_channels.insert(ix, channel_id);
            }
        };
        self.serialize(cx);
        self.update_entries(true, cx);
        cx.notify();
        cx.focus_self(window);
    }

    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
        self.collapsed_channels.binary_search(&channel_id).is_ok()
    }

    pub fn toggle_favorite_channel(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
        self.channel_store.update(cx, |store, cx| {
            store.toggle_favorite_channel(channel_id, cx);
        });
        self.persist_favorites(cx);
    }

    fn is_channel_favorited(&self, channel_id: ChannelId, cx: &App) -> bool {
        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
            .read(cx)
            .favorite_channel_ids()
            .iter()
            .map(|id| id.0)
            .collect();
        let kvp_store = KeyValueStore::global(cx);
        self.pending_favorites_serialization = cx.background_spawn(
            async move {
                let json = serde_json::to_string(&favorite_ids)?;
                kvp_store
                    .write_kvp(FAVORITE_CHANNELS_KEY.to_string(), json)
                    .await?;
                anyhow::Ok(())
            }
            .log_err(),
        );
    }

    fn leave_call(window: &mut Window, cx: &mut App) {
        ActiveCall::global(cx)
            .update(cx, |call, cx| call.hang_up(cx))
            .detach_and_prompt_err("Failed to hang up", window, cx, |_, _, _| None);
    }

    fn toggle_contact_finder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(workspace) = self.workspace.upgrade() {
            workspace.update(cx, |workspace, cx| {
                workspace.toggle_modal(window, cx, |window, cx| {
                    let mut finder = ContactFinder::new(self.user_store.clone(), window, cx);
                    finder.set_query(self.filter_editor.read(cx).text(cx), window, cx);
                    finder
                });
            });
        }
    }

    fn new_root_channel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.channel_editing_state = Some(ChannelEditingState::Create {
            location: None,
            pending_name: None,
        });
        self.update_entries(false, cx);
        self.select_channel_editor();
        window.focus(&self.channel_name_editor.focus_handle(cx), cx);
        cx.notify();
    }

    fn select_channel_editor(&mut self) {
        self.selection = self
            .entries
            .iter()
            .position(|entry| matches!(entry, ListEntry::ChannelEditor { .. }));
    }

    fn new_subchannel(
        &mut self,
        channel_id: ChannelId,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.collapsed_channels
            .retain(|channel| *channel != channel_id);
        self.channel_editing_state = Some(ChannelEditingState::Create {
            location: Some(channel_id),
            pending_name: None,
        });
        self.update_entries(false, cx);
        self.select_channel_editor();
        window.focus(&self.channel_name_editor.focus_handle(cx), cx);
        cx.notify();
    }

    fn manage_members(
        &mut self,
        channel_id: ChannelId,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, window, cx);
    }

    fn remove_selected_channel(&mut self, _: &Remove, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(channel) = self.selected_channel() {
            self.remove_channel(channel.id, window, cx)
        }
    }

    fn rename_selected_channel(
        &mut self,
        _: &SecondaryConfirm,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if let Some(channel) = self.selected_channel() {
            self.rename_channel(channel.id, window, cx);
        }
    }

    fn rename_channel(
        &mut self,
        channel_id: ChannelId,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let channel_store = self.channel_store.read(cx);
        if !channel_store.is_channel_admin(channel_id) {
            return;
        }
        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
            self.channel_editing_state = Some(ChannelEditingState::Rename {
                location: channel_id,
                pending_name: None,
            });
            self.channel_name_editor.update(cx, |editor, cx| {
                editor.set_text(channel.name.clone(), window, cx);
                editor.select_all(&Default::default(), window, cx);
            });
            window.focus(&self.channel_name_editor.focus_handle(cx), cx);
            self.update_entries(false, cx);
            self.select_channel_editor();
        }
    }

    fn open_selected_channel_notes(
        &mut self,
        _: &OpenSelectedChannelNotes,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if let Some(channel) = self.selected_channel() {
            self.open_channel_notes(channel.id, window, cx);
        }
    }

    pub fn toggle_selected_channel_favorite(
        &mut self,
        _: &ToggleSelectedChannelFavorite,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if let Some(channel) = self.selected_channel() {
            self.toggle_favorite_channel(channel.id, cx);
        }
    }

    fn set_channel_visibility(
        &mut self,
        channel_id: ChannelId,
        visibility: ChannelVisibility,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.channel_store
            .update(cx, |channel_store, cx| {
                channel_store.set_channel_visibility(channel_id, visibility, cx)
            })
            .detach_and_prompt_err("Failed to set channel visibility", window, cx, |e, _, _| match e.error_code() {
                ErrorCode::BadPublicNesting =>
                    if e.error_tag("direction") == Some("parent") {
                        Some("To make a channel public, its parent channel must be public.".to_string())
                    } else {
                        Some("To make a channel private, all of its subchannels must be private.".to_string())
                    },
                _ => None
            });
    }

    fn start_move_channel(
        &mut self,
        channel_id: ChannelId,
        _window: &mut Window,
        _cx: &mut Context<Self>,
    ) {
        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
    }

    fn start_move_selected_channel(
        &mut self,
        _: &StartMoveChannel,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if let Some(channel) = self.selected_channel() {
            self.start_move_channel(channel.id, window, cx);
        }
    }

    fn move_channel_on_clipboard(
        &mut self,
        to_channel_id: ChannelId,
        window: &mut Window,
        cx: &mut Context<CollabPanel>,
    ) {
        if let Some(clipboard) = self.channel_clipboard.take() {
            self.move_channel(clipboard.channel_id, to_channel_id, window, cx)
        }
    }

    fn move_channel(
        &self,
        channel_id: ChannelId,
        to: ChannelId,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.channel_store
            .update(cx, |channel_store, cx| {
                channel_store.move_channel(channel_id, to, cx)
            })
            .detach_and_prompt_err("Failed to move channel", window, cx, |e, _, _| {
                match e.error_code() {
                    ErrorCode::BadPublicNesting => {
                        Some("Public channels must have public parents".into())
                    }
                    ErrorCode::CircularNesting => {
                        Some("You cannot move a channel into itself".into())
                    }
                    ErrorCode::WrongMoveTarget => {
                        Some("You cannot move a channel into a different root channel".into())
                    }
                    _ => None,
                }
            })
    }

    pub fn move_channel_up(
        &mut self,
        _: &MoveChannelUp,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.reorder_selected_channel(Direction::Up, window, cx);
    }

    pub fn move_channel_down(
        &mut self,
        _: &MoveChannelDown,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.reorder_selected_channel(Direction::Down, window, cx);
    }

    fn reorder_selected_channel(
        &mut self,
        direction: Direction,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if let Some(channel) = self.selected_channel().cloned() {
            if self.selected_entry_is_favorite() {
                self.reorder_favorite(channel.id, direction, cx);
                return;
            }

            self.channel_store.update(cx, |store, cx| {
                store
                    .reorder_channel(channel.id, direction, cx)
                    .detach_and_prompt_err(
                        match direction {
                            Direction::Up => "Failed to move channel up",
                            Direction::Down => "Failed to move channel down",
                        },
                        window,
                        cx,
                        |_, _, _| None,
                    )
            });
        }
    }

    pub fn reorder_favorite(
        &mut self,
        channel_id: ChannelId,
        direction: Direction,
        cx: &mut Context<Self>,
    ) {
        self.channel_store.update(cx, |store, cx| {
            let favorite_ids = store.favorite_channel_ids();
            let Some(channel_index) = favorite_ids.iter().position(|id| *id == channel_id) else {
                return;
            };
            let target_channel_index = match direction {
                Direction::Up => channel_index.checked_sub(1),
                Direction::Down => {
                    let next = channel_index + 1;
                    (next < favorite_ids.len()).then_some(next)
                }
            };
            if let Some(target_channel_index) = target_channel_index {
                let mut new_ids = favorite_ids.to_vec();
                new_ids.swap(channel_index, target_channel_index);
                store.set_favorite_channel_ids(new_ids, cx);
            }
        });
        self.persist_favorites(cx);
    }

    fn open_channel_notes(
        &mut self,
        channel_id: ChannelId,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if let Some(workspace) = self.workspace.upgrade() {
            ChannelView::open(channel_id, None, workspace, window, cx).detach();
        }
    }

    fn show_inline_context_menu(
        &mut self,
        _: &Secondary,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let Some(bounds) = self
            .selection
            .and_then(|ix| self.list_state.bounds_for_item(ix))
        else {
            return;
        };

        if let Some(channel) = self.selected_channel() {
            self.deploy_channel_context_menu(
                bounds.center(),
                channel.id,
                self.selection.unwrap(),
                window,
                cx,
            );
            cx.stop_propagation();
            return;
        };

        if let Some(contact) = self.selected_contact() {
            self.deploy_contact_context_menu(bounds.center(), contact, window, cx);
            cx.stop_propagation();
        }
    }

    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
        let mut dispatch_context = KeyContext::new_with_defaults();
        dispatch_context.add("CollabPanel");
        dispatch_context.add("menu");

        let identifier = if self.channel_name_editor.focus_handle(cx).is_focused(window)
            || self.filter_editor.focus_handle(cx).is_focused(window)
        {
            "editing"
        } else {
            "not_editing"
        };

        dispatch_context.add(identifier);
        dispatch_context
    }

    fn selected_channel(&self) -> Option<&Arc<Channel>> {
        self.selection
            .and_then(|ix| self.entries.get(ix))
            .and_then(|entry| match entry {
                ListEntry::Channel { channel, .. } => Some(channel),
                _ => None,
            })
    }

    fn selected_entry_is_favorite(&self) -> bool {
        self.selection
            .and_then(|ix| self.entries.get(ix))
            .is_some_and(|entry| {
                matches!(
                    entry,
                    ListEntry::Channel {
                        is_favorite: true,
                        ..
                    }
                )
            })
    }

    fn selected_contact(&self) -> Option<Arc<Contact>> {
        self.selection
            .and_then(|ix| self.entries.get(ix))
            .and_then(|entry| match entry {
                ListEntry::Contact { contact, .. } => Some(contact.clone()),
                _ => None,
            })
    }

    fn show_channel_modal(
        &mut self,
        channel_id: ChannelId,
        mode: channel_modal::Mode,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let workspace = self.workspace.clone();
        let user_store = self.user_store.clone();
        let channel_store = self.channel_store.clone();

        cx.spawn_in(window, async move |_, cx| {
            workspace.update_in(cx, |workspace, window, cx| {
                workspace.toggle_modal(window, cx, |window, cx| {
                    ChannelModal::new(
                        user_store.clone(),
                        channel_store.clone(),
                        channel_id,
                        mode,
                        window,
                        cx,
                    )
                });
            })
        })
        .detach();
    }

    fn leave_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
        let Some(user_id) = self.user_store.read(cx).current_user().map(|u| u.legacy_id) else {
            return;
        };
        let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) else {
            return;
        };
        let prompt_message = format!("Are you sure you want to leave \"#{}\"?", channel.name);
        let answer = window.prompt(
            PromptLevel::Warning,
            &prompt_message,
            None,
            &["Leave", "Cancel"],
            cx,
        );
        cx.spawn_in(window, async move |this, cx| {
            if answer.await? != 0 {
                return Ok(());
            }
            this.update(cx, |this, cx| {
                this.channel_store.update(cx, |channel_store, cx| {
                    channel_store.remove_member(channel_id, user_id, cx)
                })
            })?
            .await
        })
        .detach_and_prompt_err("Failed to leave channel", window, cx, |_, _, _| None)
    }

    fn remove_channel(
        &mut self,
        channel_id: ChannelId,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let channel_store = self.channel_store.clone();
        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
            let prompt_message = format!(
                "Are you sure you want to remove the channel \"{}\"?",
                channel.name
            );
            let answer = window.prompt(
                PromptLevel::Warning,
                &prompt_message,
                None,
                &["Remove", "Cancel"],
                cx,
            );
            let workspace = self.workspace.clone();
            cx.spawn_in(window, async move |this, mut cx| {
                if answer.await? == 0 {
                    channel_store
                        .update(cx, |channels, _| channels.remove_channel(channel_id))
                        .await
                        .notify_workspace_async_err(workspace, &mut cx);
                    this.update_in(cx, |_, window, cx| cx.focus_self(window))
                        .ok();
                }
                anyhow::Ok(())
            })
            .detach();
        }
    }

    fn remove_contact(
        &mut self,
        user_id: u64,
        github_login: &str,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let user_store = self.user_store.clone();
        let prompt_message = format!(
            "Are you sure you want to remove \"{}\" from your contacts?",
            github_login
        );
        let answer = window.prompt(
            PromptLevel::Warning,
            &prompt_message,
            None,
            &["Remove", "Cancel"],
            cx,
        );
        let workspace = self.workspace.clone();
        cx.spawn_in(window, async move |_, mut cx| {
            if answer.await? == 0 {
                user_store
                    .update(cx, |store, cx| store.remove_contact(user_id, cx))
                    .await
                    .notify_workspace_async_err(workspace, &mut cx);
            }
            anyhow::Ok(())
        })
        .detach_and_prompt_err("Failed to remove contact", window, cx, |_, _, _| None);
    }

    fn respond_to_contact_request(
        &mut self,
        user_id: u64,
        accept: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.user_store
            .update(cx, |store, cx| {
                store.respond_to_contact_request(user_id, accept, cx)
            })
            .detach_and_prompt_err(
                "Failed to respond to contact request",
                window,
                cx,
                |_, _, _| None,
            );
    }

    fn respond_to_channel_invite(
        &mut self,
        channel_id: ChannelId,
        accept: bool,
        cx: &mut Context<Self>,
    ) {
        self.channel_store
            .update(cx, |store, cx| {
                store.respond_to_channel_invite(channel_id, accept, cx)
            })
            .detach();
    }

    fn call(&mut self, recipient_user_id: u64, window: &mut Window, cx: &mut Context<Self>) {
        ActiveCall::global(cx)
            .update(cx, |call, cx| {
                call.invite(recipient_user_id, Some(self.project.clone()), cx)
            })
            .detach_and_prompt_err("Call failed", window, cx, |_, _, _| None);
    }

    fn join_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
        let Some(workspace) = self.workspace.upgrade() else {
            return;
        };

        let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {
            return;
        };
        workspace::join_channel(
            channel_id,
            workspace.read(cx).app_state().clone(),
            Some(handle),
            Some(self.workspace.clone()),
            cx,
        )
        .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None)
    }

    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
        let channel_store = self.channel_store.read(cx);
        let Some(channel) = channel_store.channel_for_id(channel_id) else {
            return;
        };
        let item = ClipboardItem::new_string(channel.link(cx));
        cx.write_to_clipboard(item)
    }

    fn copy_channel_notes_link(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
        let channel_store = self.channel_store.read(cx);
        let Some(channel) = channel_store.channel_for_id(channel_id) else {
            return;
        };
        let item = ClipboardItem::new_string(channel.notes_link(None, cx));
        cx.write_to_clipboard(item)
    }

    fn render_disabled_by_organization(&mut self, _cx: &mut Context<Self>) -> Div {
        v_flex()
            .p_4()
            .gap_4()
            .size_full()
            .text_center()
            .justify_center()
            .child(Label::new(
                "Collaboration is disabled for this organization.",
            ))
    }

    fn render_signed_out(&mut self, cx: &mut Context<Self>) -> Div {
        let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";

        // Two distinct "not connected" states:
        //   - Authenticated (has credentials): user just needs to connect.
        //   - Unauthenticated (no credentials): user needs to sign in via GitHub.
        let is_authenticated = self.client.user_id().is_some();
        let status = *self.client.status().borrow();
        let is_busy = status.is_signing_in();

        let (button_id, button_label, button_icon) = if is_authenticated {
            (
                "connect",
                if is_busy { "Connecting…" } else { "Connect" },
                IconName::Public,
            )
        } else {
            (
                "sign_in",
                if is_busy {
                    "Signing in…"
                } else {
                    "Sign In with GitHub"
                },
                IconName::Github,
            )
        };

        v_flex()
            .p_4()
            .gap_4()
            .size_full()
            .text_center()
            .justify_center()
            .child(Label::new(collab_blurb))
            .child(
                Button::new(button_id, button_label)
                    .full_width()
                    .start_icon(Icon::new(button_icon).color(Color::Muted))
                    .style(ButtonStyle::Outlined)
                    .disabled(is_busy)
                    .on_click(cx.listener(|this, _, window, cx| {
                        let client = this.client.clone();
                        let workspace = this.workspace.clone();
                        cx.spawn_in(window, async move |_, mut cx| {
                            client
                                .connect(true, &mut cx)
                                .await
                                .into_response()
                                .notify_workspace_async_err(workspace, &mut cx);
                        })
                        .detach()
                    })),
            )
    }

    fn render_list_entry(
        &mut self,
        ix: usize,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> AnyElement {
        let entry = self.entries[ix].clone();

        let is_selected = self.selection == Some(ix);
        match entry {
            ListEntry::Header(section) => {
                let is_collapsed = self.collapsed_sections.contains(&section);
                self.render_header(section, is_selected, is_collapsed, cx)
                    .into_any_element()
            }
            ListEntry::Contact { contact, calling } => {
                self.mark_contact_request_accepted_notifications_read(contact.user.legacy_id, cx);
                self.render_contact(&contact, calling, is_selected, cx)
                    .into_any_element()
            }
            ListEntry::ContactPlaceholder => self
                .render_contact_placeholder(is_selected, cx)
                .into_any_element(),
            ListEntry::IncomingRequest(user) => self
                .render_contact_request(&user, true, is_selected, cx)
                .into_any_element(),
            ListEntry::OutgoingRequest(user) => self
                .render_contact_request(&user, false, is_selected, cx)
                .into_any_element(),
            ListEntry::Channel {
                channel,
                depth,
                has_children,
                string_match,
                ..
            } => self
                .render_channel(
                    &channel,
                    depth,
                    has_children,
                    is_selected,
                    ix,
                    string_match.as_ref(),
                    cx,
                )
                .into_any_element(),
            ListEntry::ChannelEditor { depth } => self
                .render_channel_editor(depth, window, cx)
                .into_any_element(),
            ListEntry::ChannelInvite(channel) => self
                .render_channel_invite(&channel, is_selected, cx)
                .into_any_element(),
            ListEntry::CallParticipant {
                user,
                peer_id,
                is_pending,
                role,
            } => self
                .render_call_participant(&user, peer_id, is_pending, role, is_selected, cx)
                .into_any_element(),
            ListEntry::ParticipantProject {
                project_id,
                worktree_root_names,
                host_user_id,
                is_last,
            } => self
                .render_participant_project(
                    project_id,
                    &worktree_root_names,
                    host_user_id,
                    is_last,
                    is_selected,
                    window,
                    cx,
                )
                .into_any_element(),
            ListEntry::ParticipantScreen { peer_id, is_last } => self
                .render_participant_screen(peer_id, is_last, is_selected, window, cx)
                .into_any_element(),
            ListEntry::ChannelNotes { channel_id } => self
                .render_channel_notes(channel_id, is_selected, window, cx)
                .into_any_element(),
        }
    }

    fn render_signed_in(&mut self, _: &mut Window, cx: &mut Context<Self>) -> Div {
        self.channel_store.update(cx, |channel_store, _| {
            channel_store.initialize();
        });

        let has_query = !self.filter_editor.read(cx).text(cx).is_empty();

        v_flex()
            .size_full()
            .gap_1()
            .child(
                h_flex()
                    .p_2()
                    .h(Tab::container_height(cx))
                    .gap_1p5()
                    .border_b_1()
                    .border_color(cx.theme().colors().border)
                    .child(
                        Icon::new(IconName::MagnifyingGlass)
                            .size(IconSize::Small)
                            .color(Color::Muted),
                    )
                    .child(self.render_filter_input(&self.filter_editor, cx))
                    .when(has_query, |this| {
                        this.pr_2p5().child(
                            IconButton::new("clear_filter", IconName::Close)
                                .shape(IconButtonShape::Square)
                                .tooltip(Tooltip::text("Clear Filter"))
                                .on_click(cx.listener(|this, _, window, cx| {
                                    this.reset_filter_editor_text(window, cx);
                                    cx.notify();
                                })),
                        )
                    }),
            )
            .child(
                list(
                    self.list_state.clone(),
                    cx.processor(Self::render_list_entry),
                )
                .size_full(),
            )
    }

    fn render_filter_input(
        &self,
        editor: &Entity<Editor>,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let settings = ThemeSettings::get_global(cx);
        let text_style = TextStyle {
            color: if editor.read(cx).read_only(cx) {
                cx.theme().colors().text_disabled
            } else {
                cx.theme().colors().text
            },
            font_family: settings.ui_font.family.clone(),
            font_features: settings.ui_font.features.clone(),
            font_fallbacks: settings.ui_font.fallbacks.clone(),
            font_size: rems(0.875).into(),
            font_weight: settings.ui_font.weight,
            font_style: FontStyle::Normal,
            line_height: relative(1.3),
            ..Default::default()
        };

        EditorElement::new(
            editor,
            EditorStyle {
                local_player: cx.theme().players().local(),
                text: text_style,
                ..Default::default()
            },
        )
    }

    fn render_header(
        &self,
        section: Section,
        is_selected: bool,
        is_collapsed: bool,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let mut channel_link = None;
        let mut channel_tooltip_text = None;
        let mut channel_icon = None;

        let text = match section {
            Section::ActiveCall => {
                let channel_name = maybe!({
                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;

                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;

                    channel_link = Some(channel.link(cx));
                    (channel_icon, channel_tooltip_text) = match channel.visibility {
                        proto::ChannelVisibility::Public => {
                            (Some("icons/public.svg"), Some("Copy public channel link."))
                        }
                        proto::ChannelVisibility::Members => {
                            (Some("icons/hash.svg"), Some("Copy private channel link."))
                        }
                    };

                    Some(channel.name.clone())
                });

                if let Some(name) = channel_name {
                    name
                } else {
                    SharedString::from("Current Call")
                }
            }
            Section::FavoriteChannels => SharedString::from("Favorites"),
            Section::ContactRequests => SharedString::from("Requests"),
            Section::Contacts => SharedString::from("Contacts"),
            Section::Channels => SharedString::from("Channels"),
            Section::ChannelInvites => SharedString::from("Invites"),
            Section::Online => SharedString::from("Online"),
            Section::Offline => SharedString::from("Offline"),
        };

        let auto_watch_state = self
            .workspace
            .upgrade()
            .map_or(AutoWatch::Off, |workspace| {
                *workspace.read(cx).auto_watch_state()
            });
        let is_auto_watching = auto_watch_state.enabled();

        let button = match section {
            Section::ActiveCall => {
                let has_auto_watch_flag = cx.has_flag::<AutoWatchFeatureFlag>();
                let show_auto_watch = has_auto_watch_flag && is_auto_watching;
                let show_copy = channel_link.is_some();

                if show_auto_watch || show_copy {
                    Some(
                        h_flex()
                            .when_some(channel_link, |this, channel_link| {
                                this.child(
                                    CopyButton::new("copy-channel-link", channel_link)
                                        .visible_on_hover("section-header")
                                        .tooltip_label("Copy Channel Link"),
                                )
                            })
                            .when(has_auto_watch_flag, |this| {
                                this.child(
                                    IconButton::new(
                                        "auto-watch-screens",
                                        if is_auto_watching {
                                            IconName::Eye
                                        } else {
                                            IconName::EyeOff
                                        },
                                    )
                                    .icon_size(IconSize::Small)
                                    .toggle_state(is_auto_watching)
                                    .selected_style(match auto_watch_state {
                                        AutoWatch::Paused => {
                                            ButtonStyle::Tinted(TintColor::Warning)
                                        }
                                        _ => ButtonStyle::Tinted(TintColor::Accent),
                                    })
                                    .when(!is_auto_watching, |this| {
                                        this.visible_on_hover("section-header")
                                    })
                                    .tooltip(Tooltip::text(match auto_watch_state {
                                        AutoWatch::Paused => {
                                            "Auto Watch Screens (paused while sharing)"
                                        }
                                        AutoWatch::Active { .. } => "Stop Auto Watching Screens",
                                        AutoWatch::Off => "Auto Watch Screens",
                                    }))
                                    .on_click(cx.listener(
                                        |this, _, window, cx| {
                                            this.workspace
                                                .update(cx, |workspace, cx| {
                                                    workspace.toggle_auto_watch(window, cx)
                                                })
                                                .ok();
                                        },
                                    )),
                                )
                            })
                            .into_any_element(),
                    )
                } else {
                    None
                }
            }
            Section::Contacts => Some(
                IconButton::new("add-contact", IconName::Plus)
                    .icon_size(IconSize::Small)
                    .on_click(
                        cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)),
                    )
                    .tooltip(Tooltip::text("Search for new contact"))
                    .into_any_element(),
            ),
            Section::Channels => {
                Some(
                    h_flex()
                        .child(
                            IconButton::new("filter-occupied-channels", IconName::ListFilter)
                                .icon_size(IconSize::Small)
                                .toggle_state(self.filter_occupied_channels)
                                .on_click(cx.listener(|this, _, _window, cx| {
                                    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_occupied_channels {
                                    "Show All Channels"
                                } else {
                                    "Show Occupied Channels"
                                })),
                        )
                        .child(
                            IconButton::new("add-channel", IconName::Plus)
                                .icon_size(IconSize::Small)
                                .on_click(cx.listener(|this, _, window, cx| {
                                    this.new_root_channel(window, cx)
                                }))
                                .tooltip(Tooltip::text("Create Channel")),
                        )
                        .into_any_element(),
                )
            }
            _ => None,
        };

        let can_collapse = match section {
            Section::ActiveCall
            | Section::Channels
            | Section::Contacts
            | Section::FavoriteChannels => false,

            Section::ChannelInvites
            | Section::ContactRequests
            | Section::Online
            | Section::Offline => true,
        };

        h_flex().w_full().group("section-header").child(
            ListHeader::new(text)
                .when(can_collapse, |header| {
                    header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
                        move |this, _, _, cx| {
                            this.toggle_section_expanded(section, cx);
                        },
                    ))
                })
                .inset(true)
                .end_slot::<AnyElement>(button)
                .toggle_state(is_selected),
        )
    }

    fn render_contact(
        &self,
        contact: &Arc<Contact>,
        calling: bool,
        is_selected: bool,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let online = contact.online;
        let busy = contact.busy || calling;
        let github_login = contact.user.github_login.clone();
        let item = ListItem::new(github_login.clone())
            .indent_level(1)
            .indent_step_size(px(20.))
            .toggle_state(is_selected)
            .child(
                h_flex()
                    .w_full()
                    .justify_between()
                    .child(render_participant_name_and_handle(&contact.user))
                    .when(calling, |el| {
                        el.child(Label::new("Calling").color(Color::Muted))
                    })
                    .when(!calling, |el| {
                        el.child(
                            IconButton::new("contact context menu", IconName::Ellipsis)
                                .icon_color(Color::Muted)
                                .visible_on_hover("")
                                .on_click(cx.listener({
                                    let contact = contact.clone();
                                    move |this, event: &ClickEvent, window, cx| {
                                        this.deploy_contact_context_menu(
                                            event.position(),
                                            contact.clone(),
                                            window,
                                            cx,
                                        );
                                    }
                                })),
                        )
                    }),
            )
            .on_secondary_mouse_down(cx.listener({
                let contact = contact.clone();
                move |this, event: &MouseDownEvent, window, cx| {
                    this.deploy_contact_context_menu(event.position, contact.clone(), window, cx);
                }
            }))
            .start_slot(
                // todo handle contacts with no avatar
                Avatar::new(contact.user.avatar_uri.clone())
                    .indicator::<AvatarAvailabilityIndicator>(if online {
                        Some(AvatarAvailabilityIndicator::new(match busy {
                            true => ui::CollaboratorAvailability::Busy,
                            false => ui::CollaboratorAvailability::Free,
                        }))
                    } else {
                        None
                    }),
            );

        div()
            .id(github_login.clone())
            .group("")
            .child(item)
            .tooltip(move |_, cx| {
                let text = if !online {
                    format!(" {} is offline", &github_login)
                } else if busy {
                    format!(" {} is on a call", &github_login)
                } else {
                    let room = ActiveCall::global(cx).read(cx).room();
                    if room.is_some() {
                        format!("Invite {} to join call", &github_login)
                    } else {
                        format!("Call {}", &github_login)
                    }
                };
                Tooltip::simple(text, cx)
            })
    }

    fn render_contact_request(
        &self,
        user: &Arc<User>,
        is_incoming: bool,
        is_selected: bool,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let github_login = user.github_login.clone();
        let user_id = user.legacy_id;
        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
        let color = if is_response_pending {
            Color::Muted
        } else {
            Color::Default
        };

        let controls = if is_incoming {
            vec![
                IconButton::new("decline-contact", IconName::Close)
                    .on_click(cx.listener(move |this, _, window, cx| {
                        this.respond_to_contact_request(user_id, false, window, cx);
                    }))
                    .icon_color(color)
                    .tooltip(Tooltip::text("Decline invite")),
                IconButton::new("accept-contact", IconName::Check)
                    .on_click(cx.listener(move |this, _, window, cx| {
                        this.respond_to_contact_request(user_id, true, window, cx);
                    }))
                    .icon_color(color)
                    .tooltip(Tooltip::text("Accept invite")),
            ]
        } else {
            let github_login = github_login.clone();
            vec![
                IconButton::new("remove_contact", IconName::Close)
                    .on_click(cx.listener(move |this, _, window, cx| {
                        this.remove_contact(user_id, &github_login, window, cx);
                    }))
                    .icon_color(color)
                    .tooltip(Tooltip::text("Cancel invite")),
            ]
        };

        ListItem::new(github_login.clone())
            .indent_level(1)
            .indent_step_size(px(20.))
            .toggle_state(is_selected)
            .child(
                h_flex()
                    .w_full()
                    .justify_between()
                    .child(Label::new(github_login))
                    .child(h_flex().children(controls)),
            )
            .start_slot(Avatar::new(user.avatar_uri.clone()))
    }

    fn render_channel_invite(
        &self,
        channel: &Arc<Channel>,
        is_selected: bool,
        cx: &mut Context<Self>,
    ) -> ListItem {
        let channel_id = channel.id;
        let response_is_pending = self
            .channel_store
            .read(cx)
            .has_pending_channel_invite_response(channel);
        let color = if response_is_pending {
            Color::Muted
        } else {
            Color::Default
        };

        let controls = [
            IconButton::new("reject-invite", IconName::Close)
                .on_click(cx.listener(move |this, _, _, cx| {
                    this.respond_to_channel_invite(channel_id, false, cx);
                }))
                .icon_color(color)
                .tooltip(Tooltip::text("Decline invite")),
            IconButton::new("accept-invite", IconName::Check)
                .on_click(cx.listener(move |this, _, _, cx| {
                    this.respond_to_channel_invite(channel_id, true, cx);
                }))
                .icon_color(color)
                .tooltip(Tooltip::text("Accept invite")),
        ];

        ListItem::new(("channel-invite", channel.id.0 as usize))
            .toggle_state(is_selected)
            .child(
                h_flex()
                    .w_full()
                    .justify_between()
                    .child(Label::new(channel.name.clone()))
                    .child(h_flex().children(controls)),
            )
            .start_slot(
                Icon::new(IconName::Hash)
                    .size(IconSize::Small)
                    .color(Color::Muted),
            )
    }

    fn render_contact_placeholder(&self, is_selected: bool, cx: &mut Context<Self>) -> ListItem {
        ListItem::new("contact-placeholder")
            .child(Icon::new(IconName::Plus))
            .child(Label::new("Add a Contact"))
            .toggle_state(is_selected)
            .on_click(cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)))
    }

    fn render_channel(
        &self,
        channel: &Channel,
        depth: usize,
        has_children: bool,
        is_selected: bool,
        ix: usize,
        string_match: Option<&StringMatch>,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let channel_id = channel.id;

        let is_active = maybe!({
            let call_channel = ActiveCall::global(cx)
                .read(cx)
                .room()?
                .read(cx)
                .channel_id()?;
            Some(call_channel == channel_id)
        })
        .unwrap_or(false);
        let channel_store = self.channel_store.read(cx);
        let is_public = channel_store
            .channel_for_id(channel_id)
            .map(|channel| channel.visibility)
            == Some(proto::ChannelVisibility::Public);
        let disclosed =
            has_children.then(|| self.collapsed_channels.binary_search(&channel.id).is_err());

        let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);

        const FACEPILE_LIMIT: usize = 3;
        let participants = self.channel_store.read(cx).channel_participants(channel_id);

        let face_pile = if participants.is_empty() {
            None
        } else {
            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
            let result = Facepile::new(
                participants
                    .iter()
                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
                    .take(FACEPILE_LIMIT)
                    .chain(if extra_count > 0 {
                        Some(
                            Label::new(format!("+{extra_count}"))
                                .ml_2()
                                .into_any_element(),
                        )
                    } else {
                        None
                    })
                    .collect::<SmallVec<_>>(),
            );

            Some(result)
        };

        let width = self
            .workspace
            .read_with(cx, |workspace, cx| {
                workspace
                    .panel_size_state::<Self>(cx)
                    .and_then(|size_state| size_state.size)
            })
            .ok()
            .flatten()
            .unwrap_or(px(240.));
        let root_id = channel.root_id();

        let is_favorited = self.is_channel_favorited(channel_id, cx);
        let (favorite_icon, favorite_color, favorite_tooltip) = if is_favorited {
            (IconName::StarFilled, Color::Accent, "Remove from Favorites")
        } else {
            (IconName::Star, Color::Default, "Add to Favorites")
        };

        let height = rems_from_px(24.);

        h_flex()
            .id(ix)
            .group("")
            .h(height)
            .w_full()
            .overflow_hidden()
            .when(!channel.is_root_channel(), |el| {
                el.on_drag(channel.clone(), move |channel, _, _, cx| {
                    cx.new(|_| DraggedChannelView {
                        channel: channel.clone(),
                        width,
                    })
                })
            })
            .drag_over::<Channel>({
                move |style, dragged_channel: &Channel, _window, cx| {
                    if dragged_channel.root_id() == root_id {
                        style.bg(cx.theme().colors().ghost_element_hover)
                    } else {
                        style
                    }
                }
            })
            .on_drop(
                cx.listener(move |this, dragged_channel: &Channel, window, cx| {
                    if dragged_channel.root_id() != root_id {
                        return;
                    }
                    this.move_channel(dragged_channel.id, channel_id, window, cx);
                }),
            )
            .child(
                ListItem::new(ix)
                    .height(height)
                    // Add one level of depth for the disclosure arrow.
                    .indent_level(depth + 1)
                    .indent_step_size(px(20.))
                    .toggle_state(is_selected || is_active)
                    .toggle(disclosed)
                    .on_toggle(cx.listener(move |this, _, window, cx| {
                        this.toggle_channel_collapsed(channel_id, window, cx)
                    }))
                    .on_click(cx.listener(move |this, _, window, cx| {
                        if is_active {
                            this.open_channel_notes(channel_id, window, cx)
                        } else {
                            this.join_channel(channel_id, window, cx)
                        }
                    }))
                    .on_secondary_mouse_down(cx.listener(
                        move |this, event: &MouseDownEvent, window, cx| {
                            this.deploy_channel_context_menu(
                                event.position,
                                channel_id,
                                ix,
                                window,
                                cx,
                            )
                        },
                    ))
                    .child(
                        h_flex()
                            .id(format!("inside-{}", channel_id.0))
                            .w_full()
                            .gap_1()
                            .child(
                                div()
                                    .relative()
                                    .child(
                                        Icon::new(if is_public {
                                            IconName::Public
                                        } else {
                                            IconName::Hash
                                        })
                                        .size(IconSize::Small)
                                        .color(Color::Muted),
                                    )
                                    .children(has_notes_notification.then(|| {
                                        div()
                                            .w_1p5()
                                            .absolute()
                                            .right(px(-1.))
                                            .top(px(-1.))
                                            .child(Indicator::dot().color(Color::Info))
                                    })),
                            )
                            .child(
                                h_flex()
                                    .id(channel_id.0 as usize)
                                    .child(match string_match {
                                        None => Label::new(channel.name.clone()).into_any_element(),
                                        Some(string_match) => HighlightedLabel::new(
                                            channel.name.clone(),
                                            string_match.positions.clone(),
                                        )
                                        .into_any_element(),
                                    })
                                    .children(face_pile.map(|face_pile| face_pile.p_1())),
                            )
                            .tooltip({
                                let channel_store = self.channel_store.clone();
                                move |_window, cx| {
                                    cx.new(|_| JoinChannelTooltip {
                                        channel_store: channel_store.clone(),
                                        channel_id,
                                        has_notes_notification,
                                    })
                                    .into()
                                }
                            }),
                    ),
            )
            .child(
                h_flex()
                    .visible_on_hover("")
                    .h_full()
                    .absolute()
                    .right_0()
                    .px_1()
                    .gap_px()
                    .rounded_l_md()
                    .bg(cx.theme().colors().background)
                    .child({
                        let focus_handle = self.focus_handle.clone();
                        IconButton::new("channel_favorite", favorite_icon)
                            .icon_size(IconSize::Small)
                            .icon_color(favorite_color)
                            .on_click(cx.listener(move |this, _, _window, cx| {
                                this.toggle_favorite_channel(channel_id, cx)
                            }))
                            .tooltip(move |_window, cx| {
                                Tooltip::for_action_in(
                                    favorite_tooltip,
                                    &ToggleSelectedChannelFavorite,
                                    &focus_handle,
                                    cx,
                                )
                            })
                    })
                    .child({
                        let focus_handle = self.focus_handle.clone();
                        IconButton::new("channel_notes", IconName::Reader)
                            .icon_size(IconSize::Small)
                            .on_click(cx.listener(move |this, _, window, cx| {
                                this.open_channel_notes(channel_id, window, cx)
                            }))
                            .tooltip(move |_window, cx| {
                                Tooltip::for_action_in(
                                    "Open Channel Notes",
                                    &OpenSelectedChannelNotes,
                                    &focus_handle,
                                    cx,
                                )
                            })
                    }),
            )
    }

    fn render_channel_editor(
        &self,
        depth: usize,
        _window: &mut Window,
        _cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let item = ListItem::new("channel-editor")
            .inset(false)
            // Add one level of depth for the disclosure arrow.
            .indent_level(depth + 1)
            .indent_step_size(px(20.))
            .start_slot(
                Icon::new(IconName::Hash)
                    .size(IconSize::Small)
                    .color(Color::Muted),
            );

        if let Some(pending_name) = self
            .channel_editing_state
            .as_ref()
            .and_then(|state| state.pending_name())
        {
            item.child(Label::new(pending_name))
        } else {
            item.child(self.channel_name_editor.clone())
        }
    }

    fn on_notification_event(
        &mut self,
        _: &Entity<NotificationStore>,
        event: &NotificationEvent,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        match event {
            NotificationEvent::NewNotification { entry } => {
                self.add_toast(entry, cx);
                cx.notify();
            }
            NotificationEvent::NotificationRemoved { entry }
            | NotificationEvent::NotificationRead { entry } => {
                self.remove_toast(entry.id, cx);
                cx.notify();
            }
            NotificationEvent::NotificationsUpdated { .. } => {
                cx.notify();
            }
        }
    }

    fn present_notification(
        &self,
        entry: &NotificationEntry,
        cx: &App,
    ) -> Option<(Option<Arc<User>>, String)> {
        let user_store = self.user_store.read(cx);
        match &entry.notification {
            Notification::ContactRequest { sender_id } => {
                let requester = user_store.get_cached_user(*sender_id)?;
                Some((
                    Some(requester.clone()),
                    format!("{} wants to add you as a contact", requester.github_login),
                ))
            }
            Notification::ContactRequestAccepted { responder_id } => {
                let responder = user_store.get_cached_user(*responder_id)?;
                Some((
                    Some(responder.clone()),
                    format!("{} accepted your contact request", responder.github_login),
                ))
            }
            Notification::ChannelInvitation {
                channel_name,
                inviter_id,
                ..
            } => {
                let inviter = user_store.get_cached_user(*inviter_id)?;
                Some((
                    Some(inviter.clone()),
                    format!(
                        "{} invited you to join the #{channel_name} channel",
                        inviter.github_login
                    ),
                ))
            }
        }
    }

    fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut Context<Self>) {
        let Some((actor, text)) = self.present_notification(entry, cx) else {
            return;
        };

        let notification = entry.notification.clone();
        let needs_response = matches!(
            notification,
            Notification::ContactRequest { .. } | Notification::ChannelInvitation { .. }
        );

        let notification_id = entry.id;

        self.current_notification_toast = Some((
            notification_id,
            cx.spawn(async move |this, cx| {
                cx.background_executor().timer(TOAST_DURATION).await;
                this.update(cx, |this, cx| this.remove_toast(notification_id, cx))
                    .ok();
            }),
        ));

        let collab_panel = cx.entity().downgrade();
        self.workspace
            .update(cx, |workspace, cx| {
                let id = NotificationId::unique::<CollabNotificationToast>();

                workspace.dismiss_notification(&id, cx);
                workspace.show_notification(id, cx, |cx| {
                    let workspace = cx.entity().downgrade();
                    cx.new(|cx| CollabNotificationToast {
                        actor,
                        text,
                        notification: needs_response.then(|| notification),
                        workspace,
                        collab_panel: collab_panel.clone(),
                        focus_handle: cx.focus_handle(),
                    })
                })
            })
            .ok();
    }

    fn mark_notification_read(&mut self, notification_id: u64, cx: &mut Context<Self>) {
        let client = self.client.clone();
        self.mark_as_read_tasks
            .entry(notification_id)
            .or_insert_with(|| {
                cx.spawn(async move |this, cx| {
                    let request_result = client
                        .request(proto::MarkNotificationRead { notification_id })
                        .await;

                    this.update(cx, |this, _| {
                        this.mark_as_read_tasks.remove(&notification_id);
                    })?;

                    request_result?;
                    Ok(())
                })
            });
    }

    fn mark_contact_request_accepted_notifications_read(
        &mut self,
        contact_user_id: u64,
        cx: &mut Context<Self>,
    ) {
        let notification_ids = self.notification_store.read_with(cx, |store, _| {
            (0..store.notification_count())
                .filter_map(|index| {
                    let entry = store.notification_at(index)?;
                    if entry.is_read {
                        return None;
                    }

                    match &entry.notification {
                        Notification::ContactRequestAccepted { responder_id }
                            if *responder_id == contact_user_id =>
                        {
                            Some(entry.id)
                        }
                        _ => None,
                    }
                })
                .collect::<Vec<_>>()
        });

        for notification_id in notification_ids {
            self.mark_notification_read(notification_id, cx);
        }
    }

    fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) {
        if let Some((current_id, _)) = &self.current_notification_toast {
            if *current_id == notification_id {
                self.dismiss_toast(cx);
            }
        }
    }

    fn dismiss_toast(&mut self, cx: &mut Context<Self>) {
        self.current_notification_toast.take();
        self.workspace
            .update(cx, |workspace, cx| {
                let id = NotificationId::unique::<CollabNotificationToast>();
                workspace.dismiss_notification(&id, cx)
            })
            .ok();
    }
}

fn render_tree_branch(
    is_last: bool,
    overdraw: bool,
    window: &mut Window,
    cx: &mut App,
) -> impl IntoElement {
    let rem_size = window.rem_size();
    let line_height = window.text_style().line_height_in_pixels(rem_size);
    let thickness = px(1.);
    let color = cx.theme().colors().icon_disabled;

    canvas(
        |_, _, _| {},
        move |bounds, _, window, _| {
            let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
            let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
            let right = bounds.right();
            let top = bounds.top();

            window.paint_quad(fill(
                Bounds::from_corners(
                    point(start_x, top),
                    point(
                        start_x + thickness,
                        if is_last {
                            start_y
                        } else {
                            bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
                        },
                    ),
                ),
                color,
            ));
            window.paint_quad(fill(
                Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
                color,
            ));
        },
    )
    .w(rem_size)
    .h(line_height - px(2.))
}

fn render_participant_name_and_handle(user: &User) -> impl IntoElement {
    Label::new(if let Some(ref display_name) = user.name {
        format!("{display_name} ({})", user.github_login)
    } else {
        user.github_login.to_string()
    })
}

impl Render for CollabPanel {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let status = *self.client.status().borrow();

        let is_collaboration_disabled = self
            .user_store
            .read(cx)
            .current_organization_configuration()
            .is_some_and(|config| !config.is_collaboration_enabled);

        v_flex()
            .key_context(self.dispatch_context(window, cx))
            .on_action(cx.listener(CollabPanel::cancel))
            .on_action(cx.listener(CollabPanel::select_next))
            .on_action(cx.listener(CollabPanel::select_previous))
            .on_action(cx.listener(CollabPanel::confirm))
            .on_action(cx.listener(CollabPanel::insert_space))
            .on_action(cx.listener(CollabPanel::remove_selected_channel))
            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
            .on_action(cx.listener(CollabPanel::rename_selected_channel))
            .on_action(cx.listener(CollabPanel::open_selected_channel_notes))
            .on_action(cx.listener(CollabPanel::toggle_selected_channel_favorite))
            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
            .on_action(cx.listener(CollabPanel::expand_selected_channel))
            .on_action(cx.listener(CollabPanel::start_move_selected_channel))
            .on_action(cx.listener(CollabPanel::move_channel_up))
            .on_action(cx.listener(CollabPanel::move_channel_down))
            .track_focus(&self.focus_handle)
            .size_full()
            .child(if is_collaboration_disabled {
                self.render_disabled_by_organization(cx)
            } else if !status.is_or_was_connected() || status.is_signing_in() {
                self.render_signed_out(cx)
            } else {
                self.render_signed_in(window, cx)
            })
            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
                deferred(
                    anchored()
                        .position(*position)
                        .anchor(gpui::Anchor::TopLeft)
                        .child(menu.clone()),
                )
                .with_priority(1)
            }))
    }
}

impl EventEmitter<PanelEvent> for CollabPanel {}

impl Panel for CollabPanel {
    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
        CollaborationPanelSettings::get_global(cx).dock
    }

    fn position_is_valid(&self, position: DockPosition) -> bool {
        matches!(position, DockPosition::Left | DockPosition::Right)
    }

    fn set_position(
        &mut self,
        position: DockPosition,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
            settings.collaboration_panel.get_or_insert_default().dock = Some(position.into())
        });
    }

    fn default_size(&self, _window: &Window, cx: &App) -> Pixels {
        CollaborationPanelSettings::get_global(cx).default_width
    }

    fn set_active(&mut self, active: bool, _window: &mut Window, cx: &mut Context<Self>) {
        if active && self.current_notification_toast.is_some() {
            self.current_notification_toast.take();
            let workspace = self.workspace.clone();
            cx.defer(move |cx| {
                workspace
                    .update(cx, |workspace, cx| {
                        let id = NotificationId::unique::<CollabNotificationToast>();
                        workspace.dismiss_notification(&id, cx)
                    })
                    .ok();
            });
        }
    }

    fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
        CollaborationPanelSettings::get_global(cx)
            .button
            .then_some(ui::IconName::UserGroup)
    }

    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
        Some("Collab Panel")
    }

    fn toggle_action(&self) -> Box<dyn gpui::Action> {
        Box::new(ToggleFocus)
    }

    fn persistent_name() -> &'static str {
        "CollabPanel"
    }

    fn panel_key() -> &'static str {
        COLLABORATION_PANEL_KEY
    }

    fn activation_priority(&self) -> u32 {
        5
    }

    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
        Some(workspace::HideStatusItem::new(|settings| {
            settings.collaboration_panel.get_or_insert_default().button = Some(false);
        }))
    }
}

impl Focusable for CollabPanel {
    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
        self.filter_editor.focus_handle(cx)
    }
}

impl PartialEq for ListEntry {
    fn eq(&self, other: &Self) -> bool {
        match self {
            ListEntry::Header(section_1) => {
                if let ListEntry::Header(section_2) = other {
                    return section_1 == section_2;
                }
            }
            ListEntry::CallParticipant { user: user_1, .. } => {
                if let ListEntry::CallParticipant { user: user_2, .. } = other {
                    return user_1.legacy_id == user_2.legacy_id;
                }
            }
            ListEntry::ParticipantProject {
                project_id: project_id_1,
                ..
            } => {
                if let ListEntry::ParticipantProject {
                    project_id: project_id_2,
                    ..
                } = other
                {
                    return project_id_1 == project_id_2;
                }
            }
            ListEntry::ParticipantScreen {
                peer_id: peer_id_1, ..
            } => {
                if let ListEntry::ParticipantScreen {
                    peer_id: peer_id_2, ..
                } = other
                {
                    return peer_id_1 == peer_id_2;
                }
            }
            ListEntry::Channel {
                channel: channel_1,
                is_favorite: is_favorite_1,
                ..
            } => {
                if let ListEntry::Channel {
                    channel: channel_2,
                    is_favorite: is_favorite_2,
                    ..
                } = other
                {
                    return channel_1.id == channel_2.id && is_favorite_1 == is_favorite_2;
                }
            }
            ListEntry::ChannelNotes { channel_id } => {
                if let ListEntry::ChannelNotes {
                    channel_id: other_id,
                } = other
                {
                    return channel_id == other_id;
                }
            }
            ListEntry::ChannelInvite(channel_1) => {
                if let ListEntry::ChannelInvite(channel_2) = other {
                    return channel_1.id == channel_2.id;
                }
            }
            ListEntry::IncomingRequest(user_1) => {
                if let ListEntry::IncomingRequest(user_2) = other {
                    return user_1.legacy_id == user_2.legacy_id;
                }
            }
            ListEntry::OutgoingRequest(user_1) => {
                if let ListEntry::OutgoingRequest(user_2) = other {
                    return user_1.legacy_id == user_2.legacy_id;
                }
            }
            ListEntry::Contact {
                contact: contact_1, ..
            } => {
                if let ListEntry::Contact {
                    contact: contact_2, ..
                } = other
                {
                    return contact_1.user.legacy_id == contact_2.user.legacy_id;
                }
            }
            ListEntry::ChannelEditor { depth } => {
                if let ListEntry::ChannelEditor { depth: other_depth } = other {
                    return depth == other_depth;
                }
            }
            ListEntry::ContactPlaceholder => {
                if let ListEntry::ContactPlaceholder = other {
                    return true;
                }
            }
        }
        false
    }
}

struct DraggedChannelView {
    channel: Channel,
    width: Pixels,
}

impl Render for DraggedChannelView {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
        h_flex()
            .font_family(ui_font)
            .bg(cx.theme().colors().background)
            .w(self.width)
            .p_1()
            .gap_1()
            .child(
                Icon::new(
                    if self.channel.visibility == proto::ChannelVisibility::Public {
                        IconName::Public
                    } else {
                        IconName::Hash
                    },
                )
                .size(IconSize::Small)
                .color(Color::Muted),
            )
            .child(Label::new(self.channel.name.clone()))
    }
}

struct JoinChannelTooltip {
    channel_store: Entity<ChannelStore>,
    channel_id: ChannelId,
    #[allow(unused)]
    has_notes_notification: bool,
}

impl Render for JoinChannelTooltip {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        tooltip_container(cx, |container, cx| {
            let participants = self
                .channel_store
                .read(cx)
                .channel_participants(self.channel_id);

            container
                .child(Label::new("Join Channel"))
                .children(participants.iter().map(|participant| {
                    h_flex()
                        .gap_2()
                        .child(Avatar::new(participant.avatar_uri.clone()))
                        .child(render_participant_name_and_handle(participant))
                }))
        })
    }
}

pub struct CollabNotificationToast {
    actor: Option<Arc<User>>,
    text: String,
    notification: Option<Notification>,
    workspace: WeakEntity<Workspace>,
    collab_panel: WeakEntity<CollabPanel>,
    focus_handle: FocusHandle,
}

impl Focusable for CollabNotificationToast {
    fn focus_handle(&self, _cx: &App) -> FocusHandle {
        self.focus_handle.clone()
    }
}

impl WorkspaceNotification for CollabNotificationToast {}

impl CollabNotificationToast {
    fn focus_collab_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
        let workspace = self.workspace.clone();
        window.defer(cx, move |window, cx| {
            workspace
                .update(cx, |workspace, cx| {
                    workspace.focus_panel::<CollabPanel>(window, cx)
                })
                .ok();
        })
    }

    fn respond(&mut self, accept: bool, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(notification) = self.notification.take() {
            self.collab_panel
                .update(cx, |collab_panel, cx| match notification {
                    Notification::ContactRequest { sender_id } => {
                        collab_panel.respond_to_contact_request(sender_id, accept, window, cx);
                    }
                    Notification::ChannelInvitation { channel_id, .. } => {
                        collab_panel.respond_to_channel_invite(ChannelId(channel_id), accept, cx);
                    }
                    Notification::ContactRequestAccepted { .. } => {}
                })
                .ok();
        }
        cx.emit(DismissEvent);
    }
}

impl Render for CollabNotificationToast {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let needs_response = self.notification.is_some();

        let accept_button = if needs_response {
            Button::new("accept", "Accept").on_click(cx.listener(|this, _, window, cx| {
                this.respond(true, window, cx);
                cx.stop_propagation();
            }))
        } else {
            Button::new("dismiss", "Dismiss").on_click(cx.listener(|_, _, _, cx| {
                cx.emit(DismissEvent);
            }))
        };

        let decline_button = if needs_response {
            Button::new("decline", "Decline").on_click(cx.listener(|this, _, window, cx| {
                this.respond(false, window, cx);
                cx.stop_propagation();
            }))
        } else {
            Button::new("close", "Close").on_click(cx.listener(|_, _, _, cx| {
                cx.emit(DismissEvent);
            }))
        };

        let avatar_uri = self
            .actor
            .as_ref()
            .map(|user| user.avatar_uri.clone())
            .unwrap_or_default();

        div()
            .id("collab_notification_toast")
            .on_click(cx.listener(|this, _, window, cx| {
                this.focus_collab_panel(window, cx);
                cx.emit(DismissEvent);
            }))
            .child(
                CollabNotification::new(avatar_uri, accept_button, decline_button)
                    .child(Label::new(self.text.clone())),
            )
    }
}

impl EventEmitter<DismissEvent> for CollabNotificationToast {}
impl EventEmitter<SuppressEvent> for CollabNotificationToast {}

#[cfg(any(test, feature = "test-support"))]
impl CollabPanel {
    pub fn entries_as_strings(&self) -> Vec<String> {
        let mut string_entries = Vec::new();
        for (index, entry) in self.entries.iter().enumerate() {
            let selected_marker = if self.selection == Some(index) {
                "  <== selected"
            } else {
                ""
            };
            match entry {
                ListEntry::Header(section) => {
                    let name = match section {
                        Section::ActiveCall => "Active Call",
                        Section::FavoriteChannels => "Favorites",
                        Section::Channels => "Channels",
                        Section::ChannelInvites => "Channel Invites",
                        Section::ContactRequests => "Contact Requests",
                        Section::Contacts => "Contacts",
                        Section::Online => "Online",
                        Section::Offline => "Offline",
                    };
                    string_entries.push(format!("[{name}]"));
                }
                ListEntry::Channel {
                    channel,
                    depth,
                    has_children,
                    ..
                } => {
                    let indent = "  ".repeat(*depth + 1);
                    let icon = if *has_children {
                        "v "
                    } else if channel.visibility == proto::ChannelVisibility::Public {
                        "🛜 "
                    } else {
                        "#️⃣ "
                    };
                    string_entries.push(format!("{indent}{icon}{}{selected_marker}", channel.name));
                }
                ListEntry::ChannelNotes { .. } => {
                    string_entries.push(format!("  (notes){selected_marker}"));
                }
                ListEntry::ChannelEditor { depth } => {
                    let indent = "  ".repeat(*depth + 1);
                    string_entries.push(format!("{indent}[editor]{selected_marker}"));
                }
                ListEntry::ChannelInvite(channel) => {
                    string_entries.push(format!("  (invite) #{}{selected_marker}", channel.name));
                }
                ListEntry::CallParticipant { user, .. } => {
                    string_entries.push(format!("  {}{selected_marker}", user.github_login));
                }
                ListEntry::ParticipantProject {
                    worktree_root_names,
                    ..
                } => {
                    string_entries.push(format!(
                        "    {}{selected_marker}",
                        worktree_root_names.join(", ")
                    ));
                }
                ListEntry::ParticipantScreen { .. } => {
                    string_entries.push(format!("    (screen){selected_marker}"));
                }
                ListEntry::IncomingRequest(user) => {
                    string_entries.push(format!(
                        "  (incoming) {}{selected_marker}",
                        user.github_login
                    ));
                }
                ListEntry::OutgoingRequest(user) => {
                    string_entries.push(format!(
                        "  (outgoing) {}{selected_marker}",
                        user.github_login
                    ));
                }
                ListEntry::Contact { contact, .. } => {
                    string_entries
                        .push(format!("  {}{selected_marker}", contact.user.github_login));
                }
                ListEntry::ContactPlaceholder => {}
            }
        }
        string_entries
    }
}
