collab_panel.rs

   1mod channel_modal;
   2mod contact_finder;
   3
   4use self::channel_modal::ChannelModal;
   5use crate::{CollaborationPanelSettings, channel_view::ChannelView};
   6use anyhow::Context as _;
   7use call::ActiveCall;
   8use channel::{Channel, ChannelEvent, ChannelStore};
   9use client::{ChannelId, Client, Contact, User, UserStore};
  10use collections::{HashMap, HashSet};
  11use contact_finder::ContactFinder;
  12use db::kvp::KeyValueStore;
  13use editor::{Editor, EditorElement, EditorStyle};
  14use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
  15use gpui::{
  16    AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, DismissEvent, Div,
  17    Empty, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, KeyContext, ListOffset,
  18    ListState, MouseDownEvent, Pixels, Point, PromptLevel, SharedString, Subscription, Task,
  19    TextStyle, WeakEntity, Window, actions, anchored, canvas, deferred, div, fill, list, point,
  20    prelude::*, px,
  21};
  22
  23use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
  24use project::{Fs, Project};
  25use rpc::{
  26    ErrorCode, ErrorExt,
  27    proto::{self, ChannelVisibility, PeerId, reorder_channel::Direction},
  28};
  29use serde::{Deserialize, Serialize};
  30use settings::Settings;
  31use smallvec::SmallVec;
  32use std::{mem, sync::Arc};
  33use theme::ActiveTheme;
  34use theme_settings::ThemeSettings;
  35use ui::{
  36    Avatar, AvatarAvailabilityIndicator, ContextMenu, CopyButton, Facepile, HighlightedLabel,
  37    IconButtonShape, Indicator, ListHeader, ListItem, Tab, Tooltip, prelude::*, tooltip_container,
  38};
  39use util::{ResultExt, TryFutureExt, maybe};
  40use workspace::{
  41    CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, OpenChannelNotesById,
  42    ScreenShare, ShareProject, Workspace,
  43    dock::{DockPosition, Panel, PanelEvent},
  44    notifications::{DetachAndPromptErr, NotifyResultExt},
  45};
  46
  47const FILTER_OCCUPIED_CHANNELS_KEY: &str = "filter_occupied_channels";
  48const FAVORITE_CHANNELS_KEY: &str = "favorite_channels";
  49
  50actions!(
  51    collab_panel,
  52    [
  53        /// Toggles the collab panel.
  54        Toggle,
  55        /// Toggles focus on the collaboration panel.
  56        ToggleFocus,
  57        /// Removes the selected channel or contact.
  58        Remove,
  59        /// Opens the context menu for the selected item.
  60        Secondary,
  61        /// Collapses the selected channel in the tree view.
  62        CollapseSelectedChannel,
  63        /// Expands the selected channel in the tree view.
  64        ExpandSelectedChannel,
  65        /// Opens the meeting notes for the selected channel in the panel.
  66        ///
  67        /// Use `collab::OpenChannelNotes` to open the channel notes for the current call.
  68        OpenSelectedChannelNotes,
  69        /// Toggles whether the selected channel is in the Favorites section.
  70        ToggleSelectedChannelFavorite,
  71        /// Starts moving a channel to a new location.
  72        StartMoveChannel,
  73        /// Moves the selected item to the current location.
  74        MoveSelected,
  75        /// Inserts a space character in the filter input.
  76        InsertSpace,
  77        /// Moves the selected channel up in the list.
  78        MoveChannelUp,
  79        /// Moves the selected channel down in the list.
  80        MoveChannelDown,
  81    ]
  82);
  83
  84#[derive(Debug, Copy, Clone, PartialEq, Eq)]
  85struct ChannelMoveClipboard {
  86    channel_id: ChannelId,
  87}
  88
  89const COLLABORATION_PANEL_KEY: &str = "CollaborationPanel";
  90
  91pub fn init(cx: &mut App) {
  92    cx.observe_new(|workspace: &mut Workspace, _, _| {
  93        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
  94            workspace.toggle_panel_focus::<CollabPanel>(window, cx);
  95            if let Some(collab_panel) = workspace.panel::<CollabPanel>(cx) {
  96                collab_panel.update(cx, |panel, cx| {
  97                    panel.filter_editor.update(cx, |editor, cx| {
  98                        if editor.snapshot(window, cx).is_focused() {
  99                            editor.select_all(&Default::default(), window, cx);
 100                        }
 101                    });
 102                })
 103            }
 104        });
 105        workspace.register_action(|workspace, _: &Toggle, window, cx| {
 106            if !workspace.toggle_panel_focus::<CollabPanel>(window, cx) {
 107                workspace.close_panel::<CollabPanel>(window, cx);
 108            }
 109        });
 110        workspace.register_action(|_, _: &OpenChannelNotes, window, cx| {
 111            let channel_id = ActiveCall::global(cx)
 112                .read(cx)
 113                .room()
 114                .and_then(|room| room.read(cx).channel_id());
 115
 116            if let Some(channel_id) = channel_id {
 117                let workspace = cx.entity();
 118                window.defer(cx, move |window, cx| {
 119                    ChannelView::open(channel_id, None, workspace, window, cx)
 120                        .detach_and_log_err(cx)
 121                });
 122            }
 123        });
 124        workspace.register_action(|_, action: &OpenChannelNotesById, window, cx| {
 125            let channel_id = client::ChannelId(action.channel_id);
 126            let workspace = cx.entity();
 127            window.defer(cx, move |window, cx| {
 128                ChannelView::open(channel_id, None, workspace, window, cx).detach_and_log_err(cx)
 129            });
 130        });
 131        // TODO: make it possible to bind this one to a held key for push to talk?
 132        // how to make "toggle_on_modifiers_press" contextual?
 133        workspace.register_action(|_, _: &Mute, _, cx| title_bar::collab::toggle_mute(cx));
 134        workspace.register_action(|_, _: &Deafen, _, cx| title_bar::collab::toggle_deafen(cx));
 135        workspace.register_action(|_, _: &LeaveCall, window, cx| {
 136            CollabPanel::leave_call(window, cx);
 137        });
 138        workspace.register_action(|workspace, _: &CopyRoomId, window, cx| {
 139            use workspace::notifications::{NotificationId, NotifyTaskExt as _};
 140
 141            struct RoomIdCopiedToast;
 142
 143            if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 144                let romo_id_fut = room.read(cx).room_id();
 145                let workspace_handle = cx.weak_entity();
 146                cx.spawn(async move |workspace, cx| {
 147                    let room_id = romo_id_fut.await.context("Failed to get livekit room")?;
 148                    workspace.update(cx, |workspace, cx| {
 149                        cx.write_to_clipboard(ClipboardItem::new_string(room_id));
 150                        workspace.show_toast(
 151                            workspace::Toast::new(
 152                                NotificationId::unique::<RoomIdCopiedToast>(),
 153                                "Room ID copied to clipboard",
 154                            )
 155                            .autohide(),
 156                            cx,
 157                        );
 158                    })
 159                })
 160                .detach_and_notify_err(workspace_handle, window, cx);
 161            } else {
 162                workspace.show_error(&"There’s no active call; join one first.", cx);
 163            }
 164        });
 165        workspace.register_action(|workspace, _: &ShareProject, window, cx| {
 166            let project = workspace.project().clone();
 167            println!("{project:?}");
 168            window.defer(cx, move |_window, cx| {
 169                ActiveCall::global(cx).update(cx, move |call, cx| {
 170                    if let Some(room) = call.room() {
 171                        println!("{room:?}");
 172                        if room.read(cx).is_sharing_project() {
 173                            call.unshare_project(project, cx).ok();
 174                        } else {
 175                            call.share_project(project, cx).detach_and_log_err(cx);
 176                        }
 177                    }
 178                });
 179            });
 180        });
 181        // TODO(jk): Is this action ever triggered?
 182        workspace.register_action(|_, _: &ScreenShare, window, cx| {
 183            let room = ActiveCall::global(cx).read(cx).room().cloned();
 184            if let Some(room) = room {
 185                window.defer(cx, move |_window, cx| {
 186                    room.update(cx, |room, cx| {
 187                        if room.is_sharing_screen() {
 188                            room.unshare_screen(true, cx).ok();
 189                        } else {
 190                            #[cfg(target_os = "linux")]
 191                            let is_wayland = gpui::guess_compositor() == "Wayland";
 192                            #[cfg(not(target_os = "linux"))]
 193                            let is_wayland = false;
 194
 195                            #[cfg(target_os = "linux")]
 196                            {
 197                                if is_wayland {
 198                                    room.share_screen_wayland(cx).detach_and_log_err(cx);
 199                                }
 200                            }
 201                            if !is_wayland {
 202                                let sources = cx.screen_capture_sources();
 203
 204                                cx.spawn(async move |room, cx| {
 205                                    let sources = sources.await??;
 206                                    let first = sources.into_iter().next();
 207                                    if let Some(first) = first {
 208                                        room.update(cx, |room, cx| room.share_screen(first, cx))?
 209                                            .await
 210                                    } else {
 211                                        Ok(())
 212                                    }
 213                                })
 214                                .detach_and_log_err(cx);
 215                            }
 216                        };
 217                    });
 218                });
 219            }
 220        });
 221    })
 222    .detach();
 223}
 224
 225#[derive(Debug)]
 226pub enum ChannelEditingState {
 227    Create {
 228        location: Option<ChannelId>,
 229        pending_name: Option<String>,
 230    },
 231    Rename {
 232        location: ChannelId,
 233        pending_name: Option<String>,
 234    },
 235}
 236
 237impl ChannelEditingState {
 238    fn pending_name(&self) -> Option<String> {
 239        match self {
 240            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
 241            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
 242        }
 243    }
 244}
 245
 246pub struct CollabPanel {
 247    fs: Arc<dyn Fs>,
 248    focus_handle: FocusHandle,
 249    channel_clipboard: Option<ChannelMoveClipboard>,
 250    pending_panel_serialization: Task<Option<()>>,
 251    pending_favorites_serialization: Task<Option<()>>,
 252    pending_filter_serialization: Task<Option<()>>,
 253    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
 254    list_state: ListState,
 255    filter_editor: Entity<Editor>,
 256    channel_name_editor: Entity<Editor>,
 257    channel_editing_state: Option<ChannelEditingState>,
 258    entries: Vec<ListEntry>,
 259    selection: Option<usize>,
 260    channel_store: Entity<ChannelStore>,
 261    user_store: Entity<UserStore>,
 262    client: Arc<Client>,
 263    project: Entity<Project>,
 264    match_candidates: Vec<StringMatchCandidate>,
 265    subscriptions: Vec<Subscription>,
 266    collapsed_sections: Vec<Section>,
 267    collapsed_channels: Vec<ChannelId>,
 268    filter_occupied_channels: bool,
 269    workspace: WeakEntity<Workspace>,
 270}
 271
 272#[derive(Serialize, Deserialize)]
 273struct SerializedCollabPanel {
 274    collapsed_channels: Option<Vec<u64>>,
 275}
 276
 277#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 278enum Section {
 279    ActiveCall,
 280    FavoriteChannels,
 281    Channels,
 282    ChannelInvites,
 283    ContactRequests,
 284    Contacts,
 285    Online,
 286    Offline,
 287}
 288
 289#[derive(Clone, Debug)]
 290enum ListEntry {
 291    Header(Section),
 292    CallParticipant {
 293        user: Arc<User>,
 294        peer_id: Option<PeerId>,
 295        is_pending: bool,
 296        role: proto::ChannelRole,
 297    },
 298    ParticipantProject {
 299        project_id: u64,
 300        worktree_root_names: Vec<String>,
 301        host_user_id: u64,
 302        is_last: bool,
 303    },
 304    ParticipantScreen {
 305        peer_id: Option<PeerId>,
 306        is_last: bool,
 307    },
 308    IncomingRequest(Arc<User>),
 309    OutgoingRequest(Arc<User>),
 310    ChannelInvite(Arc<Channel>),
 311    Channel {
 312        channel: Arc<Channel>,
 313        depth: usize,
 314        has_children: bool,
 315        is_favorite: bool,
 316        // `None` when the channel is a parent of a matched channel.
 317        string_match: Option<StringMatch>,
 318    },
 319    ChannelNotes {
 320        channel_id: ChannelId,
 321    },
 322    ChannelEditor {
 323        depth: usize,
 324    },
 325    Contact {
 326        contact: Arc<Contact>,
 327        calling: bool,
 328    },
 329    ContactPlaceholder,
 330}
 331
 332impl CollabPanel {
 333    pub fn new(
 334        workspace: &mut Workspace,
 335        window: &mut Window,
 336        cx: &mut Context<Workspace>,
 337    ) -> Entity<Self> {
 338        cx.new(|cx| {
 339            let filter_editor = cx.new(|cx| {
 340                let mut editor = Editor::single_line(window, cx);
 341                editor.set_placeholder_text("Search channels…", window, cx);
 342                editor
 343            });
 344
 345            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
 346                if let editor::EditorEvent::BufferEdited = event {
 347                    let query = this.filter_editor.read(cx).text(cx);
 348                    if !query.is_empty() {
 349                        this.selection.take();
 350                    }
 351                    this.update_entries(true, cx);
 352                    if !query.is_empty() {
 353                        this.selection = this
 354                            .entries
 355                            .iter()
 356                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
 357                    }
 358                }
 359            })
 360            .detach();
 361
 362            let channel_name_editor = cx.new(|cx| Editor::single_line(window, cx));
 363
 364            cx.subscribe_in(
 365                &channel_name_editor,
 366                window,
 367                |this: &mut Self, _, event, window, cx| {
 368                    if let editor::EditorEvent::Blurred = event {
 369                        if let Some(state) = &this.channel_editing_state
 370                            && state.pending_name().is_some()
 371                        {
 372                            return;
 373                        }
 374                        this.take_editing_state(window, cx);
 375                        this.update_entries(false, cx);
 376                        cx.notify();
 377                    }
 378                },
 379            )
 380            .detach();
 381
 382            let mut this = Self {
 383                focus_handle: cx.focus_handle(),
 384                channel_clipboard: None,
 385                fs: workspace.app_state().fs.clone(),
 386                pending_panel_serialization: Task::ready(None),
 387                pending_favorites_serialization: Task::ready(None),
 388                pending_filter_serialization: Task::ready(None),
 389                context_menu: None,
 390                list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
 391                channel_name_editor,
 392                filter_editor,
 393                entries: Vec::default(),
 394                channel_editing_state: None,
 395                selection: None,
 396                channel_store: ChannelStore::global(cx),
 397                user_store: workspace.user_store().clone(),
 398                project: workspace.project().clone(),
 399                subscriptions: Vec::default(),
 400                match_candidates: Vec::default(),
 401                collapsed_sections: vec![Section::Offline],
 402                collapsed_channels: Vec::default(),
 403                filter_occupied_channels: false,
 404                workspace: workspace.weak_handle(),
 405                client: workspace.app_state().client.clone(),
 406            };
 407
 408            this.update_entries(false, cx);
 409
 410            let active_call = ActiveCall::global(cx);
 411            this.subscriptions
 412                .push(cx.observe(&this.user_store, |this, _, cx| {
 413                    this.update_entries(true, cx)
 414                }));
 415            this.subscriptions
 416                .push(cx.observe(&this.channel_store, move |this, _, cx| {
 417                    this.update_entries(true, cx)
 418                }));
 419            this.subscriptions
 420                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
 421            this.subscriptions.push(cx.subscribe_in(
 422                &this.channel_store,
 423                window,
 424                |this, _channel_store, e, window, cx| match e {
 425                    ChannelEvent::ChannelCreated(channel_id)
 426                    | ChannelEvent::ChannelRenamed(channel_id) => {
 427                        if this.take_editing_state(window, cx) {
 428                            this.update_entries(false, cx);
 429                            this.selection = this.entries.iter().position(|entry| {
 430                                if let ListEntry::Channel { channel, .. } = entry {
 431                                    channel.id == *channel_id
 432                                } else {
 433                                    false
 434                                }
 435                            });
 436                        }
 437                    }
 438                },
 439            ));
 440
 441            this
 442        })
 443    }
 444
 445    pub async fn load(
 446        workspace: WeakEntity<Workspace>,
 447        mut cx: AsyncWindowContext,
 448    ) -> anyhow::Result<Entity<Self>> {
 449        let serialized_panel = match workspace
 450            .read_with(&cx, |workspace, _| {
 451                CollabPanel::serialization_key(workspace)
 452            })
 453            .ok()
 454            .flatten()
 455        {
 456            Some(serialization_key) => {
 457                let kvp = cx.update(|_, cx| KeyValueStore::global(cx))?;
 458                kvp.read_kvp(&serialization_key)
 459                    .context("reading collaboration panel from key value store")
 460                    .log_err()
 461                    .flatten()
 462                    .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
 463                    .transpose()
 464                    .log_err()
 465                    .flatten()
 466            }
 467            None => None,
 468        };
 469
 470        workspace.update_in(&mut cx, |workspace, window, cx| {
 471            let panel = CollabPanel::new(workspace, window, cx);
 472            if let Some(serialized_panel) = serialized_panel {
 473                panel.update(cx, |panel, cx| {
 474                    panel.collapsed_channels = serialized_panel
 475                        .collapsed_channels
 476                        .unwrap_or_default()
 477                        .iter()
 478                        .map(|cid| ChannelId(*cid))
 479                        .collect();
 480                    cx.notify();
 481                });
 482            }
 483
 484            let filter_occupied_channels = KeyValueStore::global(cx)
 485                .read_kvp(FILTER_OCCUPIED_CHANNELS_KEY)
 486                .ok()
 487                .flatten()
 488                .is_some();
 489
 490            panel.update(cx, |panel, cx| {
 491                panel.filter_occupied_channels = filter_occupied_channels;
 492
 493                if filter_occupied_channels {
 494                    panel.update_entries(false, cx);
 495                }
 496            });
 497
 498            let favorites: Vec<ChannelId> = KeyValueStore::global(cx)
 499                .read_kvp(FAVORITE_CHANNELS_KEY)
 500                .ok()
 501                .flatten()
 502                .and_then(|json| serde_json::from_str::<Vec<u64>>(&json).ok())
 503                .unwrap_or_default()
 504                .into_iter()
 505                .map(ChannelId)
 506                .collect();
 507
 508            if !favorites.is_empty() {
 509                panel.update(cx, |panel, cx| {
 510                    panel.channel_store.update(cx, |store, cx| {
 511                        store.set_favorite_channel_ids(favorites, cx);
 512                    });
 513                });
 514            }
 515
 516            panel
 517        })
 518    }
 519
 520    fn serialization_key(workspace: &Workspace) -> Option<String> {
 521        workspace
 522            .database_id()
 523            .map(|id| i64::from(id).to_string())
 524            .or(workspace.session_id())
 525            .map(|id| format!("{}-{:?}", COLLABORATION_PANEL_KEY, id))
 526    }
 527
 528    fn serialize(&mut self, cx: &mut Context<Self>) {
 529        let Some(serialization_key) = self
 530            .workspace
 531            .read_with(cx, |workspace, _| CollabPanel::serialization_key(workspace))
 532            .ok()
 533            .flatten()
 534        else {
 535            return;
 536        };
 537        let collapsed_channels = if self.collapsed_channels.is_empty() {
 538            None
 539        } else {
 540            Some(self.collapsed_channels.iter().map(|id| id.0).collect())
 541        };
 542
 543        let kvp = KeyValueStore::global(cx);
 544        self.pending_panel_serialization = cx.background_spawn(
 545            async move {
 546                kvp.write_kvp(
 547                    serialization_key,
 548                    serde_json::to_string(&SerializedCollabPanel { collapsed_channels })?,
 549                )
 550                .await?;
 551                anyhow::Ok(())
 552            }
 553            .log_err(),
 554        );
 555    }
 556
 557    fn scroll_to_item(&mut self, ix: usize) {
 558        self.list_state.scroll_to_reveal_item(ix)
 559    }
 560
 561    fn update_entries(&mut self, select_same_item: bool, cx: &mut Context<Self>) {
 562        let query = self.filter_editor.read(cx).text(cx);
 563        let fg_executor = cx.foreground_executor().clone();
 564        let executor = cx.background_executor().clone();
 565
 566        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 567        let old_entries = mem::take(&mut self.entries);
 568        let mut scroll_to_top = false;
 569
 570        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 571            self.entries.push(ListEntry::Header(Section::ActiveCall));
 572            if !old_entries
 573                .iter()
 574                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
 575            {
 576                scroll_to_top = true;
 577            }
 578
 579            if !self.collapsed_sections.contains(&Section::ActiveCall) {
 580                let room = room.read(cx);
 581
 582                if query.is_empty()
 583                    && let Some(channel_id) = room.channel_id()
 584                {
 585                    self.entries.push(ListEntry::ChannelNotes { channel_id });
 586                }
 587
 588                // Populate the active user.
 589                if let Some(user) = self.user_store.read(cx).current_user() {
 590                    self.match_candidates.clear();
 591                    self.match_candidates
 592                        .push(StringMatchCandidate::new(0, &user.github_login));
 593                    let matches = fg_executor.block_on(match_strings(
 594                        &self.match_candidates,
 595                        &query,
 596                        true,
 597                        true,
 598                        usize::MAX,
 599                        &Default::default(),
 600                        executor.clone(),
 601                    ));
 602                    if !matches.is_empty() {
 603                        let user_id = user.id;
 604                        self.entries.push(ListEntry::CallParticipant {
 605                            user,
 606                            peer_id: None,
 607                            is_pending: false,
 608                            role: room.local_participant().role,
 609                        });
 610                        let mut projects = room.local_participant().projects.iter().peekable();
 611                        while let Some(project) = projects.next() {
 612                            self.entries.push(ListEntry::ParticipantProject {
 613                                project_id: project.id,
 614                                worktree_root_names: project.worktree_root_names.clone(),
 615                                host_user_id: user_id,
 616                                is_last: projects.peek().is_none() && !room.is_sharing_screen(),
 617                            });
 618                        }
 619                        if room.is_sharing_screen() {
 620                            self.entries.push(ListEntry::ParticipantScreen {
 621                                peer_id: None,
 622                                is_last: true,
 623                            });
 624                        }
 625                    }
 626                }
 627
 628                // Populate remote participants.
 629                self.match_candidates.clear();
 630                self.match_candidates
 631                    .extend(room.remote_participants().values().map(|participant| {
 632                        StringMatchCandidate::new(
 633                            participant.user.id as usize,
 634                            &participant.user.github_login,
 635                        )
 636                    }));
 637                let mut matches = fg_executor.block_on(match_strings(
 638                    &self.match_candidates,
 639                    &query,
 640                    true,
 641                    true,
 642                    usize::MAX,
 643                    &Default::default(),
 644                    executor.clone(),
 645                ));
 646                matches.sort_by(|a, b| {
 647                    let a_is_guest = room.role_for_user(a.candidate_id as u64)
 648                        == Some(proto::ChannelRole::Guest);
 649                    let b_is_guest = room.role_for_user(b.candidate_id as u64)
 650                        == Some(proto::ChannelRole::Guest);
 651                    a_is_guest
 652                        .cmp(&b_is_guest)
 653                        .then_with(|| a.string.cmp(&b.string))
 654                });
 655                for mat in matches {
 656                    let user_id = mat.candidate_id as u64;
 657                    let participant = &room.remote_participants()[&user_id];
 658                    self.entries.push(ListEntry::CallParticipant {
 659                        user: participant.user.clone(),
 660                        peer_id: Some(participant.peer_id),
 661                        is_pending: false,
 662                        role: participant.role,
 663                    });
 664                    let mut projects = participant.projects.iter().peekable();
 665                    while let Some(project) = projects.next() {
 666                        self.entries.push(ListEntry::ParticipantProject {
 667                            project_id: project.id,
 668                            worktree_root_names: project.worktree_root_names.clone(),
 669                            host_user_id: participant.user.id,
 670                            is_last: projects.peek().is_none() && !participant.has_video_tracks(),
 671                        });
 672                    }
 673                    if participant.has_video_tracks() {
 674                        self.entries.push(ListEntry::ParticipantScreen {
 675                            peer_id: Some(participant.peer_id),
 676                            is_last: true,
 677                        });
 678                    }
 679                }
 680
 681                // Populate pending participants.
 682                self.match_candidates.clear();
 683                self.match_candidates
 684                    .extend(room.pending_participants().iter().enumerate().map(
 685                        |(id, participant)| {
 686                            StringMatchCandidate::new(id, &participant.github_login)
 687                        },
 688                    ));
 689                let matches = fg_executor.block_on(match_strings(
 690                    &self.match_candidates,
 691                    &query,
 692                    true,
 693                    true,
 694                    usize::MAX,
 695                    &Default::default(),
 696                    executor.clone(),
 697                ));
 698                self.entries
 699                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
 700                        user: room.pending_participants()[mat.candidate_id].clone(),
 701                        peer_id: None,
 702                        is_pending: true,
 703                        role: proto::ChannelRole::Member,
 704                    }));
 705            }
 706        }
 707
 708        let mut request_entries = Vec::new();
 709
 710        let channel_store = self.channel_store.read(cx);
 711        let user_store = self.user_store.read(cx);
 712
 713        let favorite_ids = channel_store.favorite_channel_ids();
 714        if !favorite_ids.is_empty() {
 715            let favorite_channels: Vec<_> = favorite_ids
 716                .iter()
 717                .filter_map(|id| channel_store.channel_for_id(*id))
 718                .collect();
 719
 720            self.match_candidates.clear();
 721            self.match_candidates.extend(
 722                favorite_channels
 723                    .iter()
 724                    .enumerate()
 725                    .map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
 726            );
 727
 728            let matches = fg_executor.block_on(match_strings(
 729                &self.match_candidates,
 730                &query,
 731                true,
 732                true,
 733                usize::MAX,
 734                &Default::default(),
 735                executor.clone(),
 736            ));
 737
 738            if !matches.is_empty() || query.is_empty() {
 739                self.entries
 740                    .push(ListEntry::Header(Section::FavoriteChannels));
 741
 742                let matches_by_candidate: HashMap<usize, &StringMatch> =
 743                    matches.iter().map(|mat| (mat.candidate_id, mat)).collect();
 744
 745                for (ix, channel) in favorite_channels.iter().enumerate() {
 746                    if !query.is_empty() && !matches_by_candidate.contains_key(&ix) {
 747                        continue;
 748                    }
 749                    self.entries.push(ListEntry::Channel {
 750                        channel: (*channel).clone(),
 751                        depth: 0,
 752                        has_children: false,
 753                        is_favorite: true,
 754                        string_match: matches_by_candidate.get(&ix).cloned().cloned(),
 755                    });
 756                }
 757            }
 758        }
 759
 760        self.entries.push(ListEntry::Header(Section::Channels));
 761
 762        if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
 763            self.match_candidates.clear();
 764            self.match_candidates.extend(
 765                channel_store
 766                    .ordered_channels()
 767                    .enumerate()
 768                    .map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
 769            );
 770            let mut channels = channel_store
 771                .ordered_channels()
 772                .map(|(_, chan)| chan)
 773                .collect::<Vec<_>>();
 774            let matches = fg_executor.block_on(match_strings(
 775                &self.match_candidates,
 776                &query,
 777                true,
 778                true,
 779                usize::MAX,
 780                &Default::default(),
 781                executor.clone(),
 782            ));
 783
 784            let matches_by_id: HashMap<_, _> = matches
 785                .iter()
 786                .map(|mat| (channels[mat.candidate_id].id, mat.clone()))
 787                .collect();
 788
 789            let channel_ids_of_matches_or_parents: HashSet<_> = matches
 790                .iter()
 791                .flat_map(|mat| {
 792                    let match_channel = channels[mat.candidate_id];
 793
 794                    match_channel
 795                        .parent_path
 796                        .iter()
 797                        .copied()
 798                        .chain(Some(match_channel.id))
 799                })
 800                .collect();
 801
 802            channels.retain(|chan| channel_ids_of_matches_or_parents.contains(&chan.id));
 803
 804            if self.filter_occupied_channels {
 805                let occupied_channel_ids_or_ancestors: HashSet<_> = channel_store
 806                    .ordered_channels()
 807                    .map(|(_, channel)| channel)
 808                    .filter(|channel| !channel_store.channel_participants(channel.id).is_empty())
 809                    .flat_map(|channel| channel.parent_path.iter().copied().chain(Some(channel.id)))
 810                    .collect();
 811                channels.retain(|channel| occupied_channel_ids_or_ancestors.contains(&channel.id));
 812            }
 813
 814            if let Some(state) = &self.channel_editing_state
 815                && matches!(state, ChannelEditingState::Create { location: None, .. })
 816            {
 817                self.entries.push(ListEntry::ChannelEditor { depth: 0 });
 818            }
 819
 820            let should_respect_collapse = query.is_empty() && !self.filter_occupied_channels;
 821            let mut collapse_depth = None;
 822
 823            for (idx, channel) in channels.into_iter().enumerate() {
 824                let depth = channel.parent_path.len();
 825
 826                if should_respect_collapse {
 827                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
 828                        collapse_depth = Some(depth);
 829                    } else if let Some(collapsed_depth) = collapse_depth {
 830                        if depth > collapsed_depth {
 831                            continue;
 832                        }
 833                        if self.is_channel_collapsed(channel.id) {
 834                            collapse_depth = Some(depth);
 835                        } else {
 836                            collapse_depth = None;
 837                        }
 838                    }
 839                }
 840
 841                let has_children = channel_store
 842                    .channel_at_index(idx + 1)
 843                    .is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id]));
 844
 845                match &self.channel_editing_state {
 846                    Some(ChannelEditingState::Create {
 847                        location: parent_id,
 848                        ..
 849                    }) if *parent_id == Some(channel.id) => {
 850                        self.entries.push(ListEntry::Channel {
 851                            channel: channel.clone(),
 852                            depth,
 853                            has_children: false,
 854                            is_favorite: false,
 855                            string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
 856                        });
 857                        self.entries
 858                            .push(ListEntry::ChannelEditor { depth: depth + 1 });
 859                    }
 860                    Some(ChannelEditingState::Rename {
 861                        location: parent_id,
 862                        ..
 863                    }) if parent_id == &channel.id => {
 864                        self.entries.push(ListEntry::ChannelEditor { depth });
 865                    }
 866                    _ => {
 867                        self.entries.push(ListEntry::Channel {
 868                            channel: channel.clone(),
 869                            depth,
 870                            has_children,
 871                            is_favorite: false,
 872                            string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
 873                        });
 874                    }
 875                }
 876            }
 877        }
 878
 879        let channel_invites = channel_store.channel_invitations();
 880        if !channel_invites.is_empty() {
 881            self.match_candidates.clear();
 882            self.match_candidates.extend(
 883                channel_invites
 884                    .iter()
 885                    .enumerate()
 886                    .map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
 887            );
 888            let matches = fg_executor.block_on(match_strings(
 889                &self.match_candidates,
 890                &query,
 891                true,
 892                true,
 893                usize::MAX,
 894                &Default::default(),
 895                executor.clone(),
 896            ));
 897            request_entries.extend(
 898                matches
 899                    .iter()
 900                    .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())),
 901            );
 902
 903            if !request_entries.is_empty() {
 904                self.entries
 905                    .push(ListEntry::Header(Section::ChannelInvites));
 906                if !self.collapsed_sections.contains(&Section::ChannelInvites) {
 907                    self.entries.append(&mut request_entries);
 908                }
 909            }
 910        }
 911
 912        self.entries.push(ListEntry::Header(Section::Contacts));
 913
 914        request_entries.clear();
 915        let incoming = user_store.incoming_contact_requests();
 916        if !incoming.is_empty() {
 917            self.match_candidates.clear();
 918            self.match_candidates.extend(
 919                incoming
 920                    .iter()
 921                    .enumerate()
 922                    .map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
 923            );
 924            let matches = fg_executor.block_on(match_strings(
 925                &self.match_candidates,
 926                &query,
 927                true,
 928                true,
 929                usize::MAX,
 930                &Default::default(),
 931                executor.clone(),
 932            ));
 933            request_entries.extend(
 934                matches
 935                    .iter()
 936                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
 937            );
 938        }
 939
 940        let outgoing = user_store.outgoing_contact_requests();
 941        if !outgoing.is_empty() {
 942            self.match_candidates.clear();
 943            self.match_candidates.extend(
 944                outgoing
 945                    .iter()
 946                    .enumerate()
 947                    .map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
 948            );
 949            let matches = fg_executor.block_on(match_strings(
 950                &self.match_candidates,
 951                &query,
 952                true,
 953                true,
 954                usize::MAX,
 955                &Default::default(),
 956                executor.clone(),
 957            ));
 958            request_entries.extend(
 959                matches
 960                    .iter()
 961                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
 962            );
 963        }
 964
 965        if !request_entries.is_empty() {
 966            self.entries
 967                .push(ListEntry::Header(Section::ContactRequests));
 968            if !self.collapsed_sections.contains(&Section::ContactRequests) {
 969                self.entries.append(&mut request_entries);
 970            }
 971        }
 972
 973        let contacts = user_store.contacts();
 974        if !contacts.is_empty() {
 975            self.match_candidates.clear();
 976            self.match_candidates.extend(
 977                contacts
 978                    .iter()
 979                    .enumerate()
 980                    .map(|(ix, contact)| StringMatchCandidate::new(ix, &contact.user.github_login)),
 981            );
 982
 983            let matches = fg_executor.block_on(match_strings(
 984                &self.match_candidates,
 985                &query,
 986                true,
 987                true,
 988                usize::MAX,
 989                &Default::default(),
 990                executor,
 991            ));
 992
 993            let (online_contacts, offline_contacts) = matches
 994                .iter()
 995                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
 996
 997            for (matches, section) in [
 998                (online_contacts, Section::Online),
 999                (offline_contacts, Section::Offline),
1000            ] {
1001                if !matches.is_empty() {
1002                    self.entries.push(ListEntry::Header(section));
1003                    if !self.collapsed_sections.contains(&section) {
1004                        let active_call = &ActiveCall::global(cx).read(cx);
1005                        for mat in matches {
1006                            let contact = &contacts[mat.candidate_id];
1007                            self.entries.push(ListEntry::Contact {
1008                                contact: contact.clone(),
1009                                calling: active_call.pending_invites().contains(&contact.user.id),
1010                            });
1011                        }
1012                    }
1013                }
1014            }
1015        }
1016
1017        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
1018            self.entries.push(ListEntry::ContactPlaceholder);
1019        }
1020
1021        if select_same_item {
1022            if let Some(prev_selected_entry) = prev_selected_entry {
1023                let prev_selection = self.selection.take();
1024                for (ix, entry) in self.entries.iter().enumerate() {
1025                    if *entry == prev_selected_entry {
1026                        self.selection = Some(ix);
1027                        break;
1028                    }
1029                }
1030                if self.selection.is_none() {
1031                    self.selection = prev_selection.and_then(|prev_ix| {
1032                        if self.entries.is_empty() {
1033                            None
1034                        } else {
1035                            Some(prev_ix.min(self.entries.len() - 1))
1036                        }
1037                    });
1038                }
1039            }
1040        } else {
1041            self.selection = self.selection.and_then(|prev_selection| {
1042                if self.entries.is_empty() {
1043                    None
1044                } else {
1045                    Some(prev_selection.min(self.entries.len() - 1))
1046                }
1047            });
1048        }
1049
1050        let old_scroll_top = self.list_state.logical_scroll_top();
1051        self.list_state.reset(self.entries.len());
1052
1053        if scroll_to_top {
1054            self.list_state.scroll_to(ListOffset::default());
1055        } else {
1056            // Attempt to maintain the same scroll position.
1057            if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
1058                let new_scroll_top = self
1059                    .entries
1060                    .iter()
1061                    .position(|entry| entry == old_top_entry)
1062                    .map(|item_ix| ListOffset {
1063                        item_ix,
1064                        offset_in_item: old_scroll_top.offset_in_item,
1065                    })
1066                    .or_else(|| {
1067                        let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
1068                        let item_ix = self
1069                            .entries
1070                            .iter()
1071                            .position(|entry| entry == entry_after_old_top)?;
1072                        Some(ListOffset {
1073                            item_ix,
1074                            offset_in_item: Pixels::ZERO,
1075                        })
1076                    })
1077                    .or_else(|| {
1078                        let entry_before_old_top =
1079                            old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
1080                        let item_ix = self
1081                            .entries
1082                            .iter()
1083                            .position(|entry| entry == entry_before_old_top)?;
1084                        Some(ListOffset {
1085                            item_ix,
1086                            offset_in_item: Pixels::ZERO,
1087                        })
1088                    });
1089
1090                self.list_state
1091                    .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
1092            }
1093        }
1094
1095        cx.notify();
1096    }
1097
1098    fn render_call_participant(
1099        &self,
1100        user: &Arc<User>,
1101        peer_id: Option<PeerId>,
1102        is_pending: bool,
1103        role: proto::ChannelRole,
1104        is_selected: bool,
1105        cx: &mut Context<Self>,
1106    ) -> ListItem {
1107        let user_id = user.id;
1108        let is_current_user =
1109            self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id);
1110        let tooltip = format!("Follow {}", user.github_login);
1111
1112        let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
1113            room.read(cx).local_participant().role == proto::ChannelRole::Admin
1114        });
1115
1116        let end_slot = if is_pending {
1117            Label::new("Calling").color(Color::Muted).into_any_element()
1118        } else if is_current_user {
1119            IconButton::new("leave-call", IconName::Exit)
1120                .icon_size(IconSize::Small)
1121                .tooltip(Tooltip::text("Leave Call"))
1122                .on_click(move |_, window, cx| Self::leave_call(window, cx))
1123                .into_any_element()
1124        } else if role == proto::ChannelRole::Guest {
1125            Label::new("Guest").color(Color::Muted).into_any_element()
1126        } else if role == proto::ChannelRole::Talker {
1127            Label::new("Mic only")
1128                .color(Color::Muted)
1129                .into_any_element()
1130        } else {
1131            Empty.into_any_element()
1132        };
1133
1134        ListItem::new(user.github_login.clone())
1135            .start_slot(Avatar::new(user.avatar_uri.clone()))
1136            .child(render_participant_name_and_handle(user))
1137            .toggle_state(is_selected)
1138            .end_slot(end_slot)
1139            .tooltip(Tooltip::text("Click to Follow"))
1140            .when_some(peer_id, |el, peer_id| {
1141                if role == proto::ChannelRole::Guest {
1142                    return el;
1143                }
1144                el.tooltip(Tooltip::text(tooltip.clone()))
1145                    .on_click(cx.listener(move |this, _, window, cx| {
1146                        this.workspace
1147                            .update(cx, |workspace, cx| workspace.follow(peer_id, window, cx))
1148                            .ok();
1149                    }))
1150            })
1151            .when(is_call_admin, |el| {
1152                el.on_secondary_mouse_down(cx.listener(
1153                    move |this, event: &MouseDownEvent, window, cx| {
1154                        this.deploy_participant_context_menu(
1155                            event.position,
1156                            user_id,
1157                            role,
1158                            window,
1159                            cx,
1160                        )
1161                    },
1162                ))
1163            })
1164    }
1165
1166    fn render_participant_project(
1167        &self,
1168        project_id: u64,
1169        worktree_root_names: &[String],
1170        host_user_id: u64,
1171        is_last: bool,
1172        is_selected: bool,
1173        window: &mut Window,
1174        cx: &mut Context<Self>,
1175    ) -> impl IntoElement {
1176        let project_name: SharedString = if worktree_root_names.is_empty() {
1177            "untitled".to_string()
1178        } else {
1179            worktree_root_names.join(", ")
1180        }
1181        .into();
1182
1183        ListItem::new(project_id as usize)
1184            .height(px(24.))
1185            .toggle_state(is_selected)
1186            .on_click(cx.listener(move |this, _, window, cx| {
1187                this.workspace
1188                    .update(cx, |workspace, cx| {
1189                        let app_state = workspace.app_state().clone();
1190                        workspace::join_in_room_project(project_id, host_user_id, app_state, cx)
1191                            .detach_and_prompt_err(
1192                                "Failed to join project",
1193                                window,
1194                                cx,
1195                                |error, _, _| Some(format!("{error:#}")),
1196                            );
1197                    })
1198                    .ok();
1199            }))
1200            .start_slot(
1201                h_flex()
1202                    .gap_1p5()
1203                    .child(render_tree_branch(is_last, false, window, cx))
1204                    .child(
1205                        Icon::new(IconName::Folder)
1206                            .size(IconSize::Small)
1207                            .color(Color::Muted),
1208                    ),
1209            )
1210            .child(Label::new(project_name.clone()))
1211            .tooltip(Tooltip::text(format!("Open {}", project_name)))
1212    }
1213
1214    fn render_participant_screen(
1215        &self,
1216        peer_id: Option<PeerId>,
1217        is_last: bool,
1218        is_selected: bool,
1219        window: &mut Window,
1220        cx: &mut Context<Self>,
1221    ) -> impl IntoElement {
1222        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
1223
1224        ListItem::new(("screen", id))
1225            .height(px(24.))
1226            .toggle_state(is_selected)
1227            .start_slot(
1228                h_flex()
1229                    .gap_1p5()
1230                    .child(render_tree_branch(is_last, false, window, cx))
1231                    .child(
1232                        Icon::new(IconName::Screen)
1233                            .size(IconSize::Small)
1234                            .color(Color::Muted),
1235                    ),
1236            )
1237            .child(Label::new("Screen"))
1238            .when_some(peer_id, |this, _| {
1239                this.on_click(cx.listener(move |this, _, window, cx| {
1240                    this.workspace
1241                        .update(cx, |workspace, cx| {
1242                            workspace.open_shared_screen(peer_id.unwrap(), window, cx)
1243                        })
1244                        .ok();
1245                }))
1246                .tooltip(Tooltip::text("Open Shared Screen"))
1247            })
1248    }
1249
1250    fn take_editing_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1251        if self.channel_editing_state.take().is_some() {
1252            self.channel_name_editor.update(cx, |editor, cx| {
1253                editor.set_text("", window, cx);
1254            });
1255            true
1256        } else {
1257            false
1258        }
1259    }
1260
1261    fn render_channel_notes(
1262        &self,
1263        channel_id: ChannelId,
1264        is_selected: bool,
1265        window: &mut Window,
1266        cx: &mut Context<Self>,
1267    ) -> impl IntoElement {
1268        let channel_store = self.channel_store.read(cx);
1269        let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
1270
1271        ListItem::new("channel-notes")
1272            .height(px(24.))
1273            .toggle_state(is_selected)
1274            .on_click(cx.listener(move |this, _, window, cx| {
1275                this.open_channel_notes(channel_id, window, cx);
1276            }))
1277            .start_slot(
1278                h_flex()
1279                    .relative()
1280                    .gap_1p5()
1281                    .child(render_tree_branch(false, true, window, cx))
1282                    .child(
1283                        h_flex()
1284                            .child(
1285                                Icon::new(IconName::Reader)
1286                                    .size(IconSize::Small)
1287                                    .color(Color::Muted),
1288                            )
1289                            .when(has_channel_buffer_changed, |this| {
1290                                this.child(
1291                                    div()
1292                                        .absolute()
1293                                        .top_neg_0p5()
1294                                        .right_0()
1295                                        .child(Indicator::dot().color(Color::Info)),
1296                                )
1297                            }),
1298                    ),
1299            )
1300            .child(Label::new("notes"))
1301            .tooltip(Tooltip::text("Open Channel Notes"))
1302    }
1303
1304    fn has_subchannels(&self, ix: usize) -> bool {
1305        self.entries.get(ix).is_some_and(|entry| {
1306            if let ListEntry::Channel { has_children, .. } = entry {
1307                *has_children
1308            } else {
1309                false
1310            }
1311        })
1312    }
1313
1314    fn deploy_participant_context_menu(
1315        &mut self,
1316        position: Point<Pixels>,
1317        user_id: u64,
1318        role: proto::ChannelRole,
1319        window: &mut Window,
1320        cx: &mut Context<Self>,
1321    ) {
1322        let this = cx.entity();
1323        if !(role == proto::ChannelRole::Guest
1324            || role == proto::ChannelRole::Talker
1325            || role == proto::ChannelRole::Member)
1326        {
1327            return;
1328        }
1329
1330        let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, _| {
1331            if role == proto::ChannelRole::Guest {
1332                context_menu = context_menu.entry(
1333                    "Grant Mic Access",
1334                    None,
1335                    window.handler_for(&this, move |_, window, cx| {
1336                        ActiveCall::global(cx)
1337                            .update(cx, |call, cx| {
1338                                let Some(room) = call.room() else {
1339                                    return Task::ready(Ok(()));
1340                                };
1341                                room.update(cx, |room, cx| {
1342                                    room.set_participant_role(
1343                                        user_id,
1344                                        proto::ChannelRole::Talker,
1345                                        cx,
1346                                    )
1347                                })
1348                            })
1349                            .detach_and_prompt_err(
1350                                "Failed to grant mic access",
1351                                window,
1352                                cx,
1353                                |_, _, _| None,
1354                            )
1355                    }),
1356                );
1357            }
1358            if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker {
1359                context_menu = context_menu.entry(
1360                    "Grant Write Access",
1361                    None,
1362                    window.handler_for(&this, move |_, window, cx| {
1363                        ActiveCall::global(cx)
1364                            .update(cx, |call, cx| {
1365                                let Some(room) = call.room() else {
1366                                    return Task::ready(Ok(()));
1367                                };
1368                                room.update(cx, |room, cx| {
1369                                    room.set_participant_role(
1370                                        user_id,
1371                                        proto::ChannelRole::Member,
1372                                        cx,
1373                                    )
1374                                })
1375                            })
1376                            .detach_and_prompt_err("Failed to grant write access", window, cx, |e, _, _| {
1377                                match e.error_code() {
1378                                    ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()),
1379                                    _ => None,
1380                                }
1381                            })
1382                    }),
1383                );
1384            }
1385            if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker {
1386                let label = if role == proto::ChannelRole::Talker {
1387                    "Mute"
1388                } else {
1389                    "Revoke Access"
1390                };
1391                context_menu = context_menu.entry(
1392                    label,
1393                    None,
1394                    window.handler_for(&this, move |_, window, cx| {
1395                        ActiveCall::global(cx)
1396                            .update(cx, |call, cx| {
1397                                let Some(room) = call.room() else {
1398                                    return Task::ready(Ok(()));
1399                                };
1400                                room.update(cx, |room, cx| {
1401                                    room.set_participant_role(
1402                                        user_id,
1403                                        proto::ChannelRole::Guest,
1404                                        cx,
1405                                    )
1406                                })
1407                            })
1408                            .detach_and_prompt_err(
1409                                "Failed to revoke access",
1410                                window,
1411                                cx,
1412                                |_, _, _| None,
1413                            )
1414                    }),
1415                );
1416            }
1417
1418            context_menu
1419        });
1420
1421        window.focus(&context_menu.focus_handle(cx), cx);
1422        let subscription = cx.subscribe_in(
1423            &context_menu,
1424            window,
1425            |this, _, _: &DismissEvent, window, cx| {
1426                if this.context_menu.as_ref().is_some_and(|context_menu| {
1427                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
1428                }) {
1429                    cx.focus_self(window);
1430                }
1431                this.context_menu.take();
1432                cx.notify();
1433            },
1434        );
1435        self.context_menu = Some((context_menu, position, subscription));
1436    }
1437
1438    fn deploy_channel_context_menu(
1439        &mut self,
1440        position: Point<Pixels>,
1441        channel_id: ChannelId,
1442        ix: usize,
1443        window: &mut Window,
1444        cx: &mut Context<Self>,
1445    ) {
1446        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
1447            self.channel_store
1448                .read(cx)
1449                .channel_for_id(clipboard.channel_id)
1450                .map(|channel| channel.name.clone())
1451        });
1452        let this = cx.entity();
1453
1454        let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, cx| {
1455            if self.has_subchannels(ix) {
1456                let expand_action_name = if self.is_channel_collapsed(channel_id) {
1457                    "Expand Subchannels"
1458                } else {
1459                    "Collapse Subchannels"
1460                };
1461                context_menu = context_menu.entry(
1462                    expand_action_name,
1463                    None,
1464                    window.handler_for(&this, move |this, window, cx| {
1465                        this.toggle_channel_collapsed(channel_id, window, cx)
1466                    }),
1467                );
1468            }
1469
1470            context_menu = context_menu
1471                .entry(
1472                    "Open Notes",
1473                    None,
1474                    window.handler_for(&this, move |this, window, cx| {
1475                        this.open_channel_notes(channel_id, window, cx)
1476                    }),
1477                )
1478                .entry(
1479                    "Copy Channel Link",
1480                    None,
1481                    window.handler_for(&this, move |this, _, cx| {
1482                        this.copy_channel_link(channel_id, cx)
1483                    }),
1484                )
1485                .entry(
1486                    "Copy Channel Notes Link",
1487                    None,
1488                    window.handler_for(&this, move |this, _, cx| {
1489                        this.copy_channel_notes_link(channel_id, cx)
1490                    }),
1491                )
1492                .separator()
1493                .entry(
1494                    if self.is_channel_favorited(channel_id, cx) {
1495                        "Remove from Favorites"
1496                    } else {
1497                        "Add to Favorites"
1498                    },
1499                    None,
1500                    window.handler_for(&this, move |this, _window, cx| {
1501                        this.toggle_favorite_channel(channel_id, cx)
1502                    }),
1503                );
1504
1505            let mut has_destructive_actions = false;
1506            if self.channel_store.read(cx).is_channel_admin(channel_id) {
1507                has_destructive_actions = true;
1508                context_menu = context_menu
1509                    .separator()
1510                    .entry(
1511                        "New Subchannel",
1512                        None,
1513                        window.handler_for(&this, move |this, window, cx| {
1514                            this.new_subchannel(channel_id, window, cx)
1515                        }),
1516                    )
1517                    .entry(
1518                        "Rename",
1519                        Some(Box::new(SecondaryConfirm)),
1520                        window.handler_for(&this, move |this, window, cx| {
1521                            this.rename_channel(channel_id, window, cx)
1522                        }),
1523                    );
1524
1525                if let Some(channel_name) = clipboard_channel_name {
1526                    context_menu = context_menu.separator().entry(
1527                        format!("Move '#{}' here", channel_name),
1528                        None,
1529                        window.handler_for(&this, move |this, window, cx| {
1530                            this.move_channel_on_clipboard(channel_id, window, cx)
1531                        }),
1532                    );
1533                }
1534
1535                if self.channel_store.read(cx).is_root_channel(channel_id) {
1536                    context_menu = context_menu.separator().entry(
1537                        "Manage Members",
1538                        None,
1539                        window.handler_for(&this, move |this, window, cx| {
1540                            this.manage_members(channel_id, window, cx)
1541                        }),
1542                    )
1543                } else {
1544                    context_menu = context_menu.entry(
1545                        "Move this channel",
1546                        None,
1547                        window.handler_for(&this, move |this, window, cx| {
1548                            this.start_move_channel(channel_id, window, cx)
1549                        }),
1550                    );
1551                    if self.channel_store.read(cx).is_public_channel(channel_id) {
1552                        context_menu = context_menu.separator().entry(
1553                            "Make Channel Private",
1554                            None,
1555                            window.handler_for(&this, move |this, window, cx| {
1556                                this.set_channel_visibility(
1557                                    channel_id,
1558                                    ChannelVisibility::Members,
1559                                    window,
1560                                    cx,
1561                                )
1562                            }),
1563                        )
1564                    } else {
1565                        context_menu = context_menu.separator().entry(
1566                            "Make Channel Public",
1567                            None,
1568                            window.handler_for(&this, move |this, window, cx| {
1569                                this.set_channel_visibility(
1570                                    channel_id,
1571                                    ChannelVisibility::Public,
1572                                    window,
1573                                    cx,
1574                                )
1575                            }),
1576                        )
1577                    }
1578                }
1579
1580                context_menu = context_menu.entry(
1581                    "Delete",
1582                    None,
1583                    window.handler_for(&this, move |this, window, cx| {
1584                        this.remove_channel(channel_id, window, cx)
1585                    }),
1586                );
1587            }
1588
1589            if self.channel_store.read(cx).is_root_channel(channel_id) {
1590                if !has_destructive_actions {
1591                    context_menu = context_menu.separator()
1592                }
1593                context_menu = context_menu.entry(
1594                    "Leave Channel",
1595                    None,
1596                    window.handler_for(&this, move |this, window, cx| {
1597                        this.leave_channel(channel_id, window, cx)
1598                    }),
1599                );
1600            }
1601
1602            context_menu
1603        });
1604
1605        window.focus(&context_menu.focus_handle(cx), cx);
1606        let subscription = cx.subscribe_in(
1607            &context_menu,
1608            window,
1609            |this, _, _: &DismissEvent, window, cx| {
1610                if this.context_menu.as_ref().is_some_and(|context_menu| {
1611                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
1612                }) {
1613                    cx.focus_self(window);
1614                }
1615                this.context_menu.take();
1616                cx.notify();
1617            },
1618        );
1619        self.context_menu = Some((context_menu, position, subscription));
1620
1621        cx.notify();
1622    }
1623
1624    fn deploy_contact_context_menu(
1625        &mut self,
1626        position: Point<Pixels>,
1627        contact: Arc<Contact>,
1628        window: &mut Window,
1629        cx: &mut Context<Self>,
1630    ) {
1631        let this = cx.entity();
1632        let in_room = ActiveCall::global(cx).read(cx).room().is_some();
1633
1634        let context_menu = ContextMenu::build(window, cx, |mut context_menu, _, _| {
1635            let user_id = contact.user.id;
1636
1637            if contact.online && !contact.busy {
1638                let label = if in_room {
1639                    format!("Invite {} to join", contact.user.github_login)
1640                } else {
1641                    format!("Call {}", contact.user.github_login)
1642                };
1643                context_menu = context_menu.entry(label, None, {
1644                    let this = this.clone();
1645                    move |window, cx| {
1646                        this.update(cx, |this, cx| {
1647                            this.call(user_id, window, cx);
1648                        });
1649                    }
1650                });
1651            }
1652
1653            context_menu.entry("Remove Contact", None, {
1654                let this = this.clone();
1655                move |window, cx| {
1656                    this.update(cx, |this, cx| {
1657                        this.remove_contact(
1658                            contact.user.id,
1659                            &contact.user.github_login,
1660                            window,
1661                            cx,
1662                        );
1663                    });
1664                }
1665            })
1666        });
1667
1668        window.focus(&context_menu.focus_handle(cx), cx);
1669        let subscription = cx.subscribe_in(
1670            &context_menu,
1671            window,
1672            |this, _, _: &DismissEvent, window, cx| {
1673                if this.context_menu.as_ref().is_some_and(|context_menu| {
1674                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
1675                }) {
1676                    cx.focus_self(window);
1677                }
1678                this.context_menu.take();
1679                cx.notify();
1680            },
1681        );
1682        self.context_menu = Some((context_menu, position, subscription));
1683
1684        cx.notify();
1685    }
1686
1687    fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1688        self.filter_editor.update(cx, |editor, cx| {
1689            if editor.buffer().read(cx).len(cx).0 > 0 {
1690                editor.set_text("", window, cx);
1691                true
1692            } else {
1693                false
1694            }
1695        })
1696    }
1697
1698    fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
1699        if cx.stop_active_drag(window) {
1700            return;
1701        } else if self.take_editing_state(window, cx) {
1702            window.focus(&self.filter_editor.focus_handle(cx), cx);
1703        } else if !self.reset_filter_editor_text(window, cx) {
1704            self.focus_handle.focus(window, cx);
1705        }
1706
1707        if self.context_menu.is_some() {
1708            self.context_menu.take();
1709            cx.notify();
1710        }
1711
1712        self.update_entries(false, cx);
1713    }
1714
1715    pub fn select_next(&mut self, _: &SelectNext, _: &mut Window, cx: &mut Context<Self>) {
1716        let ix = self.selection.map_or(0, |ix| ix + 1);
1717        if ix < self.entries.len() {
1718            self.selection = Some(ix);
1719        }
1720
1721        if let Some(ix) = self.selection {
1722            self.scroll_to_item(ix)
1723        }
1724        cx.notify();
1725    }
1726
1727    pub fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
1728        let ix = self.selection.take().unwrap_or(0);
1729        if ix > 0 {
1730            self.selection = Some(ix - 1);
1731        }
1732
1733        if let Some(ix) = self.selection {
1734            self.scroll_to_item(ix)
1735        }
1736        cx.notify();
1737    }
1738
1739    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1740        if self.confirm_channel_edit(window, cx) {
1741            return;
1742        }
1743
1744        if let Some(selection) = self.selection
1745            && let Some(entry) = self.entries.get(selection)
1746        {
1747            match entry {
1748                ListEntry::Header(section) => match section {
1749                    Section::ActiveCall => Self::leave_call(window, cx),
1750                    Section::Channels => self.new_root_channel(window, cx),
1751                    Section::Contacts => self.toggle_contact_finder(window, cx),
1752                    Section::FavoriteChannels
1753                    | Section::ContactRequests
1754                    | Section::Online
1755                    | Section::Offline
1756                    | Section::ChannelInvites => {
1757                        self.toggle_section_expanded(*section, cx);
1758                    }
1759                },
1760                ListEntry::Contact { contact, calling } => {
1761                    if contact.online && !contact.busy && !calling {
1762                        self.call(contact.user.id, window, cx);
1763                    }
1764                }
1765                ListEntry::ParticipantProject {
1766                    project_id,
1767                    host_user_id,
1768                    ..
1769                } => {
1770                    if let Some(workspace) = self.workspace.upgrade() {
1771                        let app_state = workspace.read(cx).app_state().clone();
1772                        workspace::join_in_room_project(*project_id, *host_user_id, app_state, cx)
1773                            .detach_and_prompt_err(
1774                                "Failed to join project",
1775                                window,
1776                                cx,
1777                                |error, _, _| Some(format!("{error:#}")),
1778                            );
1779                    }
1780                }
1781                ListEntry::ParticipantScreen { peer_id, .. } => {
1782                    let Some(peer_id) = peer_id else {
1783                        return;
1784                    };
1785                    if let Some(workspace) = self.workspace.upgrade() {
1786                        workspace.update(cx, |workspace, cx| {
1787                            workspace.open_shared_screen(*peer_id, window, cx)
1788                        });
1789                    }
1790                }
1791                ListEntry::Channel { channel, .. } => {
1792                    let is_active = maybe!({
1793                        let call_channel = ActiveCall::global(cx)
1794                            .read(cx)
1795                            .room()?
1796                            .read(cx)
1797                            .channel_id()?;
1798
1799                        Some(call_channel == channel.id)
1800                    })
1801                    .unwrap_or(false);
1802                    if is_active {
1803                        self.open_channel_notes(channel.id, window, cx)
1804                    } else {
1805                        self.join_channel(channel.id, window, cx)
1806                    }
1807                }
1808                ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx),
1809                ListEntry::CallParticipant { user, peer_id, .. } => {
1810                    if Some(user) == self.user_store.read(cx).current_user().as_ref() {
1811                        Self::leave_call(window, cx);
1812                    } else if let Some(peer_id) = peer_id {
1813                        self.workspace
1814                            .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx))
1815                            .ok();
1816                    }
1817                }
1818                ListEntry::IncomingRequest(user) => {
1819                    self.respond_to_contact_request(user.id, true, window, cx)
1820                }
1821                ListEntry::ChannelInvite(channel) => {
1822                    self.respond_to_channel_invite(channel.id, true, cx)
1823                }
1824                ListEntry::ChannelNotes { channel_id } => {
1825                    self.open_channel_notes(*channel_id, window, cx)
1826                }
1827                ListEntry::OutgoingRequest(_) => {}
1828                ListEntry::ChannelEditor { .. } => {}
1829            }
1830        }
1831    }
1832
1833    fn insert_space(&mut self, _: &InsertSpace, window: &mut Window, cx: &mut Context<Self>) {
1834        if self.channel_editing_state.is_some() {
1835            self.channel_name_editor.update(cx, |editor, cx| {
1836                editor.insert(" ", window, cx);
1837            });
1838        } else if self.filter_editor.focus_handle(cx).is_focused(window) {
1839            self.filter_editor.update(cx, |editor, cx| {
1840                editor.insert(" ", window, cx);
1841            });
1842        }
1843    }
1844
1845    fn confirm_channel_edit(&mut self, window: &mut Window, cx: &mut Context<CollabPanel>) -> bool {
1846        if let Some(editing_state) = &mut self.channel_editing_state {
1847            match editing_state {
1848                ChannelEditingState::Create {
1849                    location,
1850                    pending_name,
1851                    ..
1852                } => {
1853                    if pending_name.is_some() {
1854                        return false;
1855                    }
1856                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1857
1858                    *pending_name = Some(channel_name.clone());
1859
1860                    let create = self.channel_store.update(cx, |channel_store, cx| {
1861                        channel_store.create_channel(&channel_name, *location, cx)
1862                    });
1863                    if location.is_none() {
1864                        cx.spawn_in(window, async move |this, cx| {
1865                            let channel_id = create.await?;
1866                            this.update_in(cx, |this, window, cx| {
1867                                this.show_channel_modal(
1868                                    channel_id,
1869                                    channel_modal::Mode::InviteMembers,
1870                                    window,
1871                                    cx,
1872                                )
1873                            })
1874                        })
1875                        .detach_and_prompt_err(
1876                            "Failed to create channel",
1877                            window,
1878                            cx,
1879                            |_, _, _| None,
1880                        );
1881                    } else {
1882                        create.detach_and_prompt_err(
1883                            "Failed to create channel",
1884                            window,
1885                            cx,
1886                            |_, _, _| None,
1887                        );
1888                    }
1889                    cx.notify();
1890                }
1891                ChannelEditingState::Rename {
1892                    location,
1893                    pending_name,
1894                } => {
1895                    if pending_name.is_some() {
1896                        return false;
1897                    }
1898                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1899                    *pending_name = Some(channel_name.clone());
1900
1901                    self.channel_store
1902                        .update(cx, |channel_store, cx| {
1903                            channel_store.rename(*location, &channel_name, cx)
1904                        })
1905                        .detach();
1906                    cx.notify();
1907                }
1908            }
1909            cx.focus_self(window);
1910            true
1911        } else {
1912            false
1913        }
1914    }
1915
1916    fn toggle_section_expanded(&mut self, section: Section, cx: &mut Context<Self>) {
1917        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1918            self.collapsed_sections.remove(ix);
1919        } else {
1920            self.collapsed_sections.push(section);
1921        }
1922        self.update_entries(false, cx);
1923    }
1924
1925    fn collapse_selected_channel(
1926        &mut self,
1927        _: &CollapseSelectedChannel,
1928        window: &mut Window,
1929        cx: &mut Context<Self>,
1930    ) {
1931        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1932            return;
1933        };
1934
1935        if self.is_channel_collapsed(channel_id) {
1936            return;
1937        }
1938
1939        self.toggle_channel_collapsed(channel_id, window, cx);
1940    }
1941
1942    fn expand_selected_channel(
1943        &mut self,
1944        _: &ExpandSelectedChannel,
1945        window: &mut Window,
1946        cx: &mut Context<Self>,
1947    ) {
1948        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1949            return;
1950        };
1951
1952        if !self.is_channel_collapsed(id) {
1953            return;
1954        }
1955
1956        self.toggle_channel_collapsed(id, window, cx)
1957    }
1958
1959    fn toggle_channel_collapsed(
1960        &mut self,
1961        channel_id: ChannelId,
1962        window: &mut Window,
1963        cx: &mut Context<Self>,
1964    ) {
1965        match self.collapsed_channels.binary_search(&channel_id) {
1966            Ok(ix) => {
1967                self.collapsed_channels.remove(ix);
1968            }
1969            Err(ix) => {
1970                self.collapsed_channels.insert(ix, channel_id);
1971            }
1972        };
1973        self.serialize(cx);
1974        self.update_entries(true, cx);
1975        cx.notify();
1976        cx.focus_self(window);
1977    }
1978
1979    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1980        self.collapsed_channels.binary_search(&channel_id).is_ok()
1981    }
1982
1983    pub fn toggle_favorite_channel(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
1984        self.channel_store.update(cx, |store, cx| {
1985            store.toggle_favorite_channel(channel_id, cx);
1986        });
1987        self.persist_favorites(cx);
1988    }
1989
1990    fn is_channel_favorited(&self, channel_id: ChannelId, cx: &App) -> bool {
1991        self.channel_store.read(cx).is_channel_favorited(channel_id)
1992    }
1993
1994    fn persist_filter_occupied_channels(&mut self, cx: &mut Context<Self>) {
1995        let is_enabled = self.filter_occupied_channels;
1996        let kvp_store = KeyValueStore::global(cx);
1997        self.pending_filter_serialization = cx.background_spawn(
1998            async move {
1999                if is_enabled {
2000                    kvp_store
2001                        .write_kvp(FILTER_OCCUPIED_CHANNELS_KEY.to_string(), "1".to_string())
2002                        .await?;
2003                } else {
2004                    kvp_store
2005                        .delete_kvp(FILTER_OCCUPIED_CHANNELS_KEY.to_string())
2006                        .await?;
2007                }
2008                anyhow::Ok(())
2009            }
2010            .log_err(),
2011        );
2012    }
2013
2014    fn persist_favorites(&mut self, cx: &mut Context<Self>) {
2015        let favorite_ids: Vec<u64> = self
2016            .channel_store
2017            .read(cx)
2018            .favorite_channel_ids()
2019            .iter()
2020            .map(|id| id.0)
2021            .collect();
2022        let kvp_store = KeyValueStore::global(cx);
2023        self.pending_favorites_serialization = cx.background_spawn(
2024            async move {
2025                let json = serde_json::to_string(&favorite_ids)?;
2026                kvp_store
2027                    .write_kvp(FAVORITE_CHANNELS_KEY.to_string(), json)
2028                    .await?;
2029                anyhow::Ok(())
2030            }
2031            .log_err(),
2032        );
2033    }
2034
2035    fn leave_call(window: &mut Window, cx: &mut App) {
2036        ActiveCall::global(cx)
2037            .update(cx, |call, cx| call.hang_up(cx))
2038            .detach_and_prompt_err("Failed to hang up", window, cx, |_, _, _| None);
2039    }
2040
2041    fn toggle_contact_finder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2042        if let Some(workspace) = self.workspace.upgrade() {
2043            workspace.update(cx, |workspace, cx| {
2044                workspace.toggle_modal(window, cx, |window, cx| {
2045                    let mut finder = ContactFinder::new(self.user_store.clone(), window, cx);
2046                    finder.set_query(self.filter_editor.read(cx).text(cx), window, cx);
2047                    finder
2048                });
2049            });
2050        }
2051    }
2052
2053    fn new_root_channel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2054        self.channel_editing_state = Some(ChannelEditingState::Create {
2055            location: None,
2056            pending_name: None,
2057        });
2058        self.update_entries(false, cx);
2059        self.select_channel_editor();
2060        window.focus(&self.channel_name_editor.focus_handle(cx), cx);
2061        cx.notify();
2062    }
2063
2064    fn select_channel_editor(&mut self) {
2065        self.selection = self
2066            .entries
2067            .iter()
2068            .position(|entry| matches!(entry, ListEntry::ChannelEditor { .. }));
2069    }
2070
2071    fn new_subchannel(
2072        &mut self,
2073        channel_id: ChannelId,
2074        window: &mut Window,
2075        cx: &mut Context<Self>,
2076    ) {
2077        self.collapsed_channels
2078            .retain(|channel| *channel != channel_id);
2079        self.channel_editing_state = Some(ChannelEditingState::Create {
2080            location: Some(channel_id),
2081            pending_name: None,
2082        });
2083        self.update_entries(false, cx);
2084        self.select_channel_editor();
2085        window.focus(&self.channel_name_editor.focus_handle(cx), cx);
2086        cx.notify();
2087    }
2088
2089    fn manage_members(
2090        &mut self,
2091        channel_id: ChannelId,
2092        window: &mut Window,
2093        cx: &mut Context<Self>,
2094    ) {
2095        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, window, cx);
2096    }
2097
2098    fn remove_selected_channel(&mut self, _: &Remove, window: &mut Window, cx: &mut Context<Self>) {
2099        if let Some(channel) = self.selected_channel() {
2100            self.remove_channel(channel.id, window, cx)
2101        }
2102    }
2103
2104    fn rename_selected_channel(
2105        &mut self,
2106        _: &SecondaryConfirm,
2107        window: &mut Window,
2108        cx: &mut Context<Self>,
2109    ) {
2110        if let Some(channel) = self.selected_channel() {
2111            self.rename_channel(channel.id, window, cx);
2112        }
2113    }
2114
2115    fn rename_channel(
2116        &mut self,
2117        channel_id: ChannelId,
2118        window: &mut Window,
2119        cx: &mut Context<Self>,
2120    ) {
2121        let channel_store = self.channel_store.read(cx);
2122        if !channel_store.is_channel_admin(channel_id) {
2123            return;
2124        }
2125        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
2126            self.channel_editing_state = Some(ChannelEditingState::Rename {
2127                location: channel_id,
2128                pending_name: None,
2129            });
2130            self.channel_name_editor.update(cx, |editor, cx| {
2131                editor.set_text(channel.name.clone(), window, cx);
2132                editor.select_all(&Default::default(), window, cx);
2133            });
2134            window.focus(&self.channel_name_editor.focus_handle(cx), cx);
2135            self.update_entries(false, cx);
2136            self.select_channel_editor();
2137        }
2138    }
2139
2140    fn open_selected_channel_notes(
2141        &mut self,
2142        _: &OpenSelectedChannelNotes,
2143        window: &mut Window,
2144        cx: &mut Context<Self>,
2145    ) {
2146        if let Some(channel) = self.selected_channel() {
2147            self.open_channel_notes(channel.id, window, cx);
2148        }
2149    }
2150
2151    pub fn toggle_selected_channel_favorite(
2152        &mut self,
2153        _: &ToggleSelectedChannelFavorite,
2154        _window: &mut Window,
2155        cx: &mut Context<Self>,
2156    ) {
2157        if let Some(channel) = self.selected_channel() {
2158            self.toggle_favorite_channel(channel.id, cx);
2159        }
2160    }
2161
2162    fn set_channel_visibility(
2163        &mut self,
2164        channel_id: ChannelId,
2165        visibility: ChannelVisibility,
2166        window: &mut Window,
2167        cx: &mut Context<Self>,
2168    ) {
2169        self.channel_store
2170            .update(cx, |channel_store, cx| {
2171                channel_store.set_channel_visibility(channel_id, visibility, cx)
2172            })
2173            .detach_and_prompt_err("Failed to set channel visibility", window, cx, |e, _, _| match e.error_code() {
2174                ErrorCode::BadPublicNesting =>
2175                    if e.error_tag("direction") == Some("parent") {
2176                        Some("To make a channel public, its parent channel must be public.".to_string())
2177                    } else {
2178                        Some("To make a channel private, all of its subchannels must be private.".to_string())
2179                    },
2180                _ => None
2181            });
2182    }
2183
2184    fn start_move_channel(
2185        &mut self,
2186        channel_id: ChannelId,
2187        _window: &mut Window,
2188        _cx: &mut Context<Self>,
2189    ) {
2190        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
2191    }
2192
2193    fn start_move_selected_channel(
2194        &mut self,
2195        _: &StartMoveChannel,
2196        window: &mut Window,
2197        cx: &mut Context<Self>,
2198    ) {
2199        if let Some(channel) = self.selected_channel() {
2200            self.start_move_channel(channel.id, window, cx);
2201        }
2202    }
2203
2204    fn move_channel_on_clipboard(
2205        &mut self,
2206        to_channel_id: ChannelId,
2207        window: &mut Window,
2208        cx: &mut Context<CollabPanel>,
2209    ) {
2210        if let Some(clipboard) = self.channel_clipboard.take() {
2211            self.move_channel(clipboard.channel_id, to_channel_id, window, cx)
2212        }
2213    }
2214
2215    fn move_channel(
2216        &self,
2217        channel_id: ChannelId,
2218        to: ChannelId,
2219        window: &mut Window,
2220        cx: &mut Context<Self>,
2221    ) {
2222        self.channel_store
2223            .update(cx, |channel_store, cx| {
2224                channel_store.move_channel(channel_id, to, cx)
2225            })
2226            .detach_and_prompt_err("Failed to move channel", window, cx, |e, _, _| {
2227                match e.error_code() {
2228                    ErrorCode::BadPublicNesting => {
2229                        Some("Public channels must have public parents".into())
2230                    }
2231                    ErrorCode::CircularNesting => {
2232                        Some("You cannot move a channel into itself".into())
2233                    }
2234                    ErrorCode::WrongMoveTarget => {
2235                        Some("You cannot move a channel into a different root channel".into())
2236                    }
2237                    _ => None,
2238                }
2239            })
2240    }
2241
2242    pub fn move_channel_up(
2243        &mut self,
2244        _: &MoveChannelUp,
2245        window: &mut Window,
2246        cx: &mut Context<Self>,
2247    ) {
2248        self.reorder_selected_channel(Direction::Up, window, cx);
2249    }
2250
2251    pub fn move_channel_down(
2252        &mut self,
2253        _: &MoveChannelDown,
2254        window: &mut Window,
2255        cx: &mut Context<Self>,
2256    ) {
2257        self.reorder_selected_channel(Direction::Down, window, cx);
2258    }
2259
2260    fn reorder_selected_channel(
2261        &mut self,
2262        direction: Direction,
2263        window: &mut Window,
2264        cx: &mut Context<Self>,
2265    ) {
2266        if let Some(channel) = self.selected_channel().cloned() {
2267            if self.selected_entry_is_favorite() {
2268                self.reorder_favorite(channel.id, direction, cx);
2269                return;
2270            }
2271
2272            self.channel_store.update(cx, |store, cx| {
2273                store
2274                    .reorder_channel(channel.id, direction, cx)
2275                    .detach_and_prompt_err(
2276                        match direction {
2277                            Direction::Up => "Failed to move channel up",
2278                            Direction::Down => "Failed to move channel down",
2279                        },
2280                        window,
2281                        cx,
2282                        |_, _, _| None,
2283                    )
2284            });
2285        }
2286    }
2287
2288    pub fn reorder_favorite(
2289        &mut self,
2290        channel_id: ChannelId,
2291        direction: Direction,
2292        cx: &mut Context<Self>,
2293    ) {
2294        self.channel_store.update(cx, |store, cx| {
2295            let favorite_ids = store.favorite_channel_ids();
2296            let Some(channel_index) = favorite_ids.iter().position(|id| *id == channel_id) else {
2297                return;
2298            };
2299            let target_channel_index = match direction {
2300                Direction::Up => channel_index.checked_sub(1),
2301                Direction::Down => {
2302                    let next = channel_index + 1;
2303                    (next < favorite_ids.len()).then_some(next)
2304                }
2305            };
2306            if let Some(target_channel_index) = target_channel_index {
2307                let mut new_ids = favorite_ids.to_vec();
2308                new_ids.swap(channel_index, target_channel_index);
2309                store.set_favorite_channel_ids(new_ids, cx);
2310            }
2311        });
2312        self.persist_favorites(cx);
2313    }
2314
2315    fn open_channel_notes(
2316        &mut self,
2317        channel_id: ChannelId,
2318        window: &mut Window,
2319        cx: &mut Context<Self>,
2320    ) {
2321        if let Some(workspace) = self.workspace.upgrade() {
2322            ChannelView::open(channel_id, None, workspace, window, cx).detach();
2323        }
2324    }
2325
2326    fn show_inline_context_menu(
2327        &mut self,
2328        _: &Secondary,
2329        window: &mut Window,
2330        cx: &mut Context<Self>,
2331    ) {
2332        let Some(bounds) = self
2333            .selection
2334            .and_then(|ix| self.list_state.bounds_for_item(ix))
2335        else {
2336            return;
2337        };
2338
2339        if let Some(channel) = self.selected_channel() {
2340            self.deploy_channel_context_menu(
2341                bounds.center(),
2342                channel.id,
2343                self.selection.unwrap(),
2344                window,
2345                cx,
2346            );
2347            cx.stop_propagation();
2348            return;
2349        };
2350
2351        if let Some(contact) = self.selected_contact() {
2352            self.deploy_contact_context_menu(bounds.center(), contact, window, cx);
2353            cx.stop_propagation();
2354        }
2355    }
2356
2357    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
2358        let mut dispatch_context = KeyContext::new_with_defaults();
2359        dispatch_context.add("CollabPanel");
2360        dispatch_context.add("menu");
2361
2362        let identifier = if self.channel_name_editor.focus_handle(cx).is_focused(window)
2363            || self.filter_editor.focus_handle(cx).is_focused(window)
2364        {
2365            "editing"
2366        } else {
2367            "not_editing"
2368        };
2369
2370        dispatch_context.add(identifier);
2371        dispatch_context
2372    }
2373
2374    fn selected_channel(&self) -> Option<&Arc<Channel>> {
2375        self.selection
2376            .and_then(|ix| self.entries.get(ix))
2377            .and_then(|entry| match entry {
2378                ListEntry::Channel { channel, .. } => Some(channel),
2379                _ => None,
2380            })
2381    }
2382
2383    fn selected_entry_is_favorite(&self) -> bool {
2384        self.selection
2385            .and_then(|ix| self.entries.get(ix))
2386            .is_some_and(|entry| {
2387                matches!(
2388                    entry,
2389                    ListEntry::Channel {
2390                        is_favorite: true,
2391                        ..
2392                    }
2393                )
2394            })
2395    }
2396
2397    fn selected_contact(&self) -> Option<Arc<Contact>> {
2398        self.selection
2399            .and_then(|ix| self.entries.get(ix))
2400            .and_then(|entry| match entry {
2401                ListEntry::Contact { contact, .. } => Some(contact.clone()),
2402                _ => None,
2403            })
2404    }
2405
2406    fn show_channel_modal(
2407        &mut self,
2408        channel_id: ChannelId,
2409        mode: channel_modal::Mode,
2410        window: &mut Window,
2411        cx: &mut Context<Self>,
2412    ) {
2413        let workspace = self.workspace.clone();
2414        let user_store = self.user_store.clone();
2415        let channel_store = self.channel_store.clone();
2416
2417        cx.spawn_in(window, async move |_, cx| {
2418            workspace.update_in(cx, |workspace, window, cx| {
2419                workspace.toggle_modal(window, cx, |window, cx| {
2420                    ChannelModal::new(
2421                        user_store.clone(),
2422                        channel_store.clone(),
2423                        channel_id,
2424                        mode,
2425                        window,
2426                        cx,
2427                    )
2428                });
2429            })
2430        })
2431        .detach();
2432    }
2433
2434    fn leave_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
2435        let Some(user_id) = self.user_store.read(cx).current_user().map(|u| u.id) else {
2436            return;
2437        };
2438        let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) else {
2439            return;
2440        };
2441        let prompt_message = format!("Are you sure you want to leave \"#{}\"?", channel.name);
2442        let answer = window.prompt(
2443            PromptLevel::Warning,
2444            &prompt_message,
2445            None,
2446            &["Leave", "Cancel"],
2447            cx,
2448        );
2449        cx.spawn_in(window, async move |this, cx| {
2450            if answer.await? != 0 {
2451                return Ok(());
2452            }
2453            this.update(cx, |this, cx| {
2454                this.channel_store.update(cx, |channel_store, cx| {
2455                    channel_store.remove_member(channel_id, user_id, cx)
2456                })
2457            })?
2458            .await
2459        })
2460        .detach_and_prompt_err("Failed to leave channel", window, cx, |_, _, _| None)
2461    }
2462
2463    fn remove_channel(
2464        &mut self,
2465        channel_id: ChannelId,
2466        window: &mut Window,
2467        cx: &mut Context<Self>,
2468    ) {
2469        let channel_store = self.channel_store.clone();
2470        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2471            let prompt_message = format!(
2472                "Are you sure you want to remove the channel \"{}\"?",
2473                channel.name
2474            );
2475            let answer = window.prompt(
2476                PromptLevel::Warning,
2477                &prompt_message,
2478                None,
2479                &["Remove", "Cancel"],
2480                cx,
2481            );
2482            let workspace = self.workspace.clone();
2483            cx.spawn_in(window, async move |this, mut cx| {
2484                if answer.await? == 0 {
2485                    channel_store
2486                        .update(cx, |channels, _| channels.remove_channel(channel_id))
2487                        .await
2488                        .notify_workspace_async_err(workspace, &mut cx);
2489                    this.update_in(cx, |_, window, cx| cx.focus_self(window))
2490                        .ok();
2491                }
2492                anyhow::Ok(())
2493            })
2494            .detach();
2495        }
2496    }
2497
2498    fn remove_contact(
2499        &mut self,
2500        user_id: u64,
2501        github_login: &str,
2502        window: &mut Window,
2503        cx: &mut Context<Self>,
2504    ) {
2505        let user_store = self.user_store.clone();
2506        let prompt_message = format!(
2507            "Are you sure you want to remove \"{}\" from your contacts?",
2508            github_login
2509        );
2510        let answer = window.prompt(
2511            PromptLevel::Warning,
2512            &prompt_message,
2513            None,
2514            &["Remove", "Cancel"],
2515            cx,
2516        );
2517        let workspace = self.workspace.clone();
2518        cx.spawn_in(window, async move |_, mut cx| {
2519            if answer.await? == 0 {
2520                user_store
2521                    .update(cx, |store, cx| store.remove_contact(user_id, cx))
2522                    .await
2523                    .notify_workspace_async_err(workspace, &mut cx);
2524            }
2525            anyhow::Ok(())
2526        })
2527        .detach_and_prompt_err("Failed to remove contact", window, cx, |_, _, _| None);
2528    }
2529
2530    fn respond_to_contact_request(
2531        &mut self,
2532        user_id: u64,
2533        accept: bool,
2534        window: &mut Window,
2535        cx: &mut Context<Self>,
2536    ) {
2537        self.user_store
2538            .update(cx, |store, cx| {
2539                store.respond_to_contact_request(user_id, accept, cx)
2540            })
2541            .detach_and_prompt_err(
2542                "Failed to respond to contact request",
2543                window,
2544                cx,
2545                |_, _, _| None,
2546            );
2547    }
2548
2549    fn respond_to_channel_invite(
2550        &mut self,
2551        channel_id: ChannelId,
2552        accept: bool,
2553        cx: &mut Context<Self>,
2554    ) {
2555        self.channel_store
2556            .update(cx, |store, cx| {
2557                store.respond_to_channel_invite(channel_id, accept, cx)
2558            })
2559            .detach();
2560    }
2561
2562    fn call(&mut self, recipient_user_id: u64, window: &mut Window, cx: &mut Context<Self>) {
2563        ActiveCall::global(cx)
2564            .update(cx, |call, cx| {
2565                call.invite(recipient_user_id, Some(self.project.clone()), cx)
2566            })
2567            .detach_and_prompt_err("Call failed", window, cx, |_, _, _| None);
2568    }
2569
2570    fn join_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
2571        let Some(workspace) = self.workspace.upgrade() else {
2572            return;
2573        };
2574
2575        let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {
2576            return;
2577        };
2578        workspace::join_channel(
2579            channel_id,
2580            workspace.read(cx).app_state().clone(),
2581            Some(handle),
2582            Some(self.workspace.clone()),
2583            cx,
2584        )
2585        .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None)
2586    }
2587
2588    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
2589        let channel_store = self.channel_store.read(cx);
2590        let Some(channel) = channel_store.channel_for_id(channel_id) else {
2591            return;
2592        };
2593        let item = ClipboardItem::new_string(channel.link(cx));
2594        cx.write_to_clipboard(item)
2595    }
2596
2597    fn copy_channel_notes_link(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
2598        let channel_store = self.channel_store.read(cx);
2599        let Some(channel) = channel_store.channel_for_id(channel_id) else {
2600            return;
2601        };
2602        let item = ClipboardItem::new_string(channel.notes_link(None, cx));
2603        cx.write_to_clipboard(item)
2604    }
2605
2606    fn render_signed_out(&mut self, cx: &mut Context<Self>) -> Div {
2607        let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
2608
2609        // Two distinct "not connected" states:
2610        //   - Authenticated (has credentials): user just needs to connect.
2611        //   - Unauthenticated (no credentials): user needs to sign in via GitHub.
2612        let is_authenticated = self.client.user_id().is_some();
2613        let status = *self.client.status().borrow();
2614        let is_busy = status.is_signing_in();
2615
2616        let (button_id, button_label, button_icon) = if is_authenticated {
2617            (
2618                "connect",
2619                if is_busy { "Connecting…" } else { "Connect" },
2620                IconName::Public,
2621            )
2622        } else {
2623            (
2624                "sign_in",
2625                if is_busy {
2626                    "Signing in…"
2627                } else {
2628                    "Sign In with GitHub"
2629                },
2630                IconName::Github,
2631            )
2632        };
2633
2634        v_flex()
2635            .p_4()
2636            .gap_4()
2637            .size_full()
2638            .text_center()
2639            .justify_center()
2640            .child(Label::new(collab_blurb))
2641            .child(
2642                Button::new(button_id, button_label)
2643                    .full_width()
2644                    .start_icon(Icon::new(button_icon).color(Color::Muted))
2645                    .style(ButtonStyle::Outlined)
2646                    .disabled(is_busy)
2647                    .on_click(cx.listener(|this, _, window, cx| {
2648                        let client = this.client.clone();
2649                        let workspace = this.workspace.clone();
2650                        cx.spawn_in(window, async move |_, mut cx| {
2651                            client
2652                                .connect(true, &mut cx)
2653                                .await
2654                                .into_response()
2655                                .notify_workspace_async_err(workspace, &mut cx);
2656                        })
2657                        .detach()
2658                    })),
2659            )
2660    }
2661
2662    fn render_list_entry(
2663        &mut self,
2664        ix: usize,
2665        window: &mut Window,
2666        cx: &mut Context<Self>,
2667    ) -> AnyElement {
2668        let entry = &self.entries[ix];
2669
2670        let is_selected = self.selection == Some(ix);
2671        match entry {
2672            ListEntry::Header(section) => {
2673                let is_collapsed = self.collapsed_sections.contains(section);
2674                self.render_header(*section, is_selected, is_collapsed, cx)
2675                    .into_any_element()
2676            }
2677            ListEntry::Contact { contact, calling } => self
2678                .render_contact(contact, *calling, is_selected, cx)
2679                .into_any_element(),
2680            ListEntry::ContactPlaceholder => self
2681                .render_contact_placeholder(is_selected, cx)
2682                .into_any_element(),
2683            ListEntry::IncomingRequest(user) => self
2684                .render_contact_request(user, true, is_selected, cx)
2685                .into_any_element(),
2686            ListEntry::OutgoingRequest(user) => self
2687                .render_contact_request(user, false, is_selected, cx)
2688                .into_any_element(),
2689            ListEntry::Channel {
2690                channel,
2691                depth,
2692                has_children,
2693                string_match,
2694                ..
2695            } => self
2696                .render_channel(
2697                    channel,
2698                    *depth,
2699                    *has_children,
2700                    is_selected,
2701                    ix,
2702                    string_match.as_ref(),
2703                    cx,
2704                )
2705                .into_any_element(),
2706            ListEntry::ChannelEditor { depth } => self
2707                .render_channel_editor(*depth, window, cx)
2708                .into_any_element(),
2709            ListEntry::ChannelInvite(channel) => self
2710                .render_channel_invite(channel, is_selected, cx)
2711                .into_any_element(),
2712            ListEntry::CallParticipant {
2713                user,
2714                peer_id,
2715                is_pending,
2716                role,
2717            } => self
2718                .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
2719                .into_any_element(),
2720            ListEntry::ParticipantProject {
2721                project_id,
2722                worktree_root_names,
2723                host_user_id,
2724                is_last,
2725            } => self
2726                .render_participant_project(
2727                    *project_id,
2728                    worktree_root_names,
2729                    *host_user_id,
2730                    *is_last,
2731                    is_selected,
2732                    window,
2733                    cx,
2734                )
2735                .into_any_element(),
2736            ListEntry::ParticipantScreen { peer_id, is_last } => self
2737                .render_participant_screen(*peer_id, *is_last, is_selected, window, cx)
2738                .into_any_element(),
2739            ListEntry::ChannelNotes { channel_id } => self
2740                .render_channel_notes(*channel_id, is_selected, window, cx)
2741                .into_any_element(),
2742        }
2743    }
2744
2745    fn render_signed_in(&mut self, _: &mut Window, cx: &mut Context<Self>) -> Div {
2746        self.channel_store.update(cx, |channel_store, _| {
2747            channel_store.initialize();
2748        });
2749
2750        let has_query = !self.filter_editor.read(cx).text(cx).is_empty();
2751
2752        v_flex()
2753            .size_full()
2754            .gap_1()
2755            .child(
2756                h_flex()
2757                    .p_2()
2758                    .h(Tab::container_height(cx))
2759                    .gap_1p5()
2760                    .border_b_1()
2761                    .border_color(cx.theme().colors().border)
2762                    .child(
2763                        Icon::new(IconName::MagnifyingGlass)
2764                            .size(IconSize::Small)
2765                            .color(Color::Muted),
2766                    )
2767                    .child(self.render_filter_input(&self.filter_editor, cx))
2768                    .when(has_query, |this| {
2769                        this.pr_2p5().child(
2770                            IconButton::new("clear_filter", IconName::Close)
2771                                .shape(IconButtonShape::Square)
2772                                .tooltip(Tooltip::text("Clear Filter"))
2773                                .on_click(cx.listener(|this, _, window, cx| {
2774                                    this.reset_filter_editor_text(window, cx);
2775                                    cx.notify();
2776                                })),
2777                        )
2778                    }),
2779            )
2780            .child(
2781                list(
2782                    self.list_state.clone(),
2783                    cx.processor(Self::render_list_entry),
2784                )
2785                .size_full(),
2786            )
2787    }
2788
2789    fn render_filter_input(
2790        &self,
2791        editor: &Entity<Editor>,
2792        cx: &mut Context<Self>,
2793    ) -> impl IntoElement {
2794        let settings = ThemeSettings::get_global(cx);
2795        let text_style = TextStyle {
2796            color: if editor.read(cx).read_only(cx) {
2797                cx.theme().colors().text_disabled
2798            } else {
2799                cx.theme().colors().text
2800            },
2801            font_family: settings.ui_font.family.clone(),
2802            font_features: settings.ui_font.features.clone(),
2803            font_fallbacks: settings.ui_font.fallbacks.clone(),
2804            font_size: rems(0.875).into(),
2805            font_weight: settings.ui_font.weight,
2806            font_style: FontStyle::Normal,
2807            line_height: relative(1.3),
2808            ..Default::default()
2809        };
2810
2811        EditorElement::new(
2812            editor,
2813            EditorStyle {
2814                local_player: cx.theme().players().local(),
2815                text: text_style,
2816                ..Default::default()
2817            },
2818        )
2819    }
2820
2821    fn render_header(
2822        &self,
2823        section: Section,
2824        is_selected: bool,
2825        is_collapsed: bool,
2826        cx: &mut Context<Self>,
2827    ) -> impl IntoElement {
2828        let mut channel_link = None;
2829        let mut channel_tooltip_text = None;
2830        let mut channel_icon = None;
2831
2832        let text = match section {
2833            Section::ActiveCall => {
2834                let channel_name = maybe!({
2835                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2836
2837                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2838
2839                    channel_link = Some(channel.link(cx));
2840                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2841                        proto::ChannelVisibility::Public => {
2842                            (Some("icons/public.svg"), Some("Copy public channel link."))
2843                        }
2844                        proto::ChannelVisibility::Members => {
2845                            (Some("icons/hash.svg"), Some("Copy private channel link."))
2846                        }
2847                    };
2848
2849                    Some(channel.name.as_ref())
2850                });
2851
2852                if let Some(name) = channel_name {
2853                    SharedString::from(name.to_string())
2854                } else {
2855                    SharedString::from("Current Call")
2856                }
2857            }
2858            Section::FavoriteChannels => SharedString::from("Favorites"),
2859            Section::ContactRequests => SharedString::from("Requests"),
2860            Section::Contacts => SharedString::from("Contacts"),
2861            Section::Channels => SharedString::from("Channels"),
2862            Section::ChannelInvites => SharedString::from("Invites"),
2863            Section::Online => SharedString::from("Online"),
2864            Section::Offline => SharedString::from("Offline"),
2865        };
2866
2867        let button = match section {
2868            Section::ActiveCall => channel_link.map(|channel_link| {
2869                CopyButton::new("copy-channel-link", channel_link)
2870                    .visible_on_hover("section-header")
2871                    .tooltip_label("Copy Channel Link")
2872                    .into_any_element()
2873            }),
2874            Section::Contacts => Some(
2875                IconButton::new("add-contact", IconName::Plus)
2876                    .icon_size(IconSize::Small)
2877                    .on_click(
2878                        cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)),
2879                    )
2880                    .tooltip(Tooltip::text("Search for new contact"))
2881                    .into_any_element(),
2882            ),
2883            Section::Channels => {
2884                Some(
2885                    h_flex()
2886                        .child(
2887                            IconButton::new("filter-occupied-channels", IconName::ListFilter)
2888                                .icon_size(IconSize::Small)
2889                                .toggle_state(self.filter_occupied_channels)
2890                                .on_click(cx.listener(|this, _, _window, cx| {
2891                                    this.filter_occupied_channels = !this.filter_occupied_channels;
2892                                    this.update_entries(true, cx);
2893                                    this.persist_filter_occupied_channels(cx);
2894                                }))
2895                                .tooltip(Tooltip::text(if self.filter_occupied_channels {
2896                                    "Show All Channels"
2897                                } else {
2898                                    "Show Occupied Channels"
2899                                })),
2900                        )
2901                        .child(
2902                            IconButton::new("add-channel", IconName::Plus)
2903                                .icon_size(IconSize::Small)
2904                                .on_click(cx.listener(|this, _, window, cx| {
2905                                    this.new_root_channel(window, cx)
2906                                }))
2907                                .tooltip(Tooltip::text("Create Channel")),
2908                        )
2909                        .into_any_element(),
2910                )
2911            }
2912            _ => None,
2913        };
2914
2915        let can_collapse = match section {
2916            Section::ActiveCall
2917            | Section::Channels
2918            | Section::Contacts
2919            | Section::FavoriteChannels => false,
2920
2921            Section::ChannelInvites
2922            | Section::ContactRequests
2923            | Section::Online
2924            | Section::Offline => true,
2925        };
2926
2927        h_flex().w_full().group("section-header").child(
2928            ListHeader::new(text)
2929                .when(can_collapse, |header| {
2930                    header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
2931                        move |this, _, _, cx| {
2932                            this.toggle_section_expanded(section, cx);
2933                        },
2934                    ))
2935                })
2936                .inset(true)
2937                .end_slot::<AnyElement>(button)
2938                .toggle_state(is_selected),
2939        )
2940    }
2941
2942    fn render_contact(
2943        &self,
2944        contact: &Arc<Contact>,
2945        calling: bool,
2946        is_selected: bool,
2947        cx: &mut Context<Self>,
2948    ) -> impl IntoElement {
2949        let online = contact.online;
2950        let busy = contact.busy || calling;
2951        let github_login = contact.user.github_login.clone();
2952        let item = ListItem::new(github_login.clone())
2953            .indent_level(1)
2954            .indent_step_size(px(20.))
2955            .toggle_state(is_selected)
2956            .child(
2957                h_flex()
2958                    .w_full()
2959                    .justify_between()
2960                    .child(render_participant_name_and_handle(&contact.user))
2961                    .when(calling, |el| {
2962                        el.child(Label::new("Calling").color(Color::Muted))
2963                    })
2964                    .when(!calling, |el| {
2965                        el.child(
2966                            IconButton::new("contact context menu", IconName::Ellipsis)
2967                                .icon_color(Color::Muted)
2968                                .visible_on_hover("")
2969                                .on_click(cx.listener({
2970                                    let contact = contact.clone();
2971                                    move |this, event: &ClickEvent, window, cx| {
2972                                        this.deploy_contact_context_menu(
2973                                            event.position(),
2974                                            contact.clone(),
2975                                            window,
2976                                            cx,
2977                                        );
2978                                    }
2979                                })),
2980                        )
2981                    }),
2982            )
2983            .on_secondary_mouse_down(cx.listener({
2984                let contact = contact.clone();
2985                move |this, event: &MouseDownEvent, window, cx| {
2986                    this.deploy_contact_context_menu(event.position, contact.clone(), window, cx);
2987                }
2988            }))
2989            .start_slot(
2990                // todo handle contacts with no avatar
2991                Avatar::new(contact.user.avatar_uri.clone())
2992                    .indicator::<AvatarAvailabilityIndicator>(if online {
2993                        Some(AvatarAvailabilityIndicator::new(match busy {
2994                            true => ui::CollaboratorAvailability::Busy,
2995                            false => ui::CollaboratorAvailability::Free,
2996                        }))
2997                    } else {
2998                        None
2999                    }),
3000            );
3001
3002        div()
3003            .id(github_login.clone())
3004            .group("")
3005            .child(item)
3006            .tooltip(move |_, cx| {
3007                let text = if !online {
3008                    format!(" {} is offline", &github_login)
3009                } else if busy {
3010                    format!(" {} is on a call", &github_login)
3011                } else {
3012                    let room = ActiveCall::global(cx).read(cx).room();
3013                    if room.is_some() {
3014                        format!("Invite {} to join call", &github_login)
3015                    } else {
3016                        format!("Call {}", &github_login)
3017                    }
3018                };
3019                Tooltip::simple(text, cx)
3020            })
3021    }
3022
3023    fn render_contact_request(
3024        &self,
3025        user: &Arc<User>,
3026        is_incoming: bool,
3027        is_selected: bool,
3028        cx: &mut Context<Self>,
3029    ) -> impl IntoElement {
3030        let github_login = user.github_login.clone();
3031        let user_id = user.id;
3032        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
3033        let color = if is_response_pending {
3034            Color::Muted
3035        } else {
3036            Color::Default
3037        };
3038
3039        let controls = if is_incoming {
3040            vec![
3041                IconButton::new("decline-contact", IconName::Close)
3042                    .on_click(cx.listener(move |this, _, window, cx| {
3043                        this.respond_to_contact_request(user_id, false, window, cx);
3044                    }))
3045                    .icon_color(color)
3046                    .tooltip(Tooltip::text("Decline invite")),
3047                IconButton::new("accept-contact", IconName::Check)
3048                    .on_click(cx.listener(move |this, _, window, cx| {
3049                        this.respond_to_contact_request(user_id, true, window, cx);
3050                    }))
3051                    .icon_color(color)
3052                    .tooltip(Tooltip::text("Accept invite")),
3053            ]
3054        } else {
3055            let github_login = github_login.clone();
3056            vec![
3057                IconButton::new("remove_contact", IconName::Close)
3058                    .on_click(cx.listener(move |this, _, window, cx| {
3059                        this.remove_contact(user_id, &github_login, window, cx);
3060                    }))
3061                    .icon_color(color)
3062                    .tooltip(Tooltip::text("Cancel invite")),
3063            ]
3064        };
3065
3066        ListItem::new(github_login.clone())
3067            .indent_level(1)
3068            .indent_step_size(px(20.))
3069            .toggle_state(is_selected)
3070            .child(
3071                h_flex()
3072                    .w_full()
3073                    .justify_between()
3074                    .child(Label::new(github_login))
3075                    .child(h_flex().children(controls)),
3076            )
3077            .start_slot(Avatar::new(user.avatar_uri.clone()))
3078    }
3079
3080    fn render_channel_invite(
3081        &self,
3082        channel: &Arc<Channel>,
3083        is_selected: bool,
3084        cx: &mut Context<Self>,
3085    ) -> ListItem {
3086        let channel_id = channel.id;
3087        let response_is_pending = self
3088            .channel_store
3089            .read(cx)
3090            .has_pending_channel_invite_response(channel);
3091        let color = if response_is_pending {
3092            Color::Muted
3093        } else {
3094            Color::Default
3095        };
3096
3097        let controls = [
3098            IconButton::new("reject-invite", IconName::Close)
3099                .on_click(cx.listener(move |this, _, _, cx| {
3100                    this.respond_to_channel_invite(channel_id, false, cx);
3101                }))
3102                .icon_color(color)
3103                .tooltip(Tooltip::text("Decline invite")),
3104            IconButton::new("accept-invite", IconName::Check)
3105                .on_click(cx.listener(move |this, _, _, cx| {
3106                    this.respond_to_channel_invite(channel_id, true, cx);
3107                }))
3108                .icon_color(color)
3109                .tooltip(Tooltip::text("Accept invite")),
3110        ];
3111
3112        ListItem::new(("channel-invite", channel.id.0 as usize))
3113            .toggle_state(is_selected)
3114            .child(
3115                h_flex()
3116                    .w_full()
3117                    .justify_between()
3118                    .child(Label::new(channel.name.clone()))
3119                    .child(h_flex().children(controls)),
3120            )
3121            .start_slot(
3122                Icon::new(IconName::Hash)
3123                    .size(IconSize::Small)
3124                    .color(Color::Muted),
3125            )
3126    }
3127
3128    fn render_contact_placeholder(&self, is_selected: bool, cx: &mut Context<Self>) -> ListItem {
3129        ListItem::new("contact-placeholder")
3130            .child(Icon::new(IconName::Plus))
3131            .child(Label::new("Add a Contact"))
3132            .toggle_state(is_selected)
3133            .on_click(cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)))
3134    }
3135
3136    fn render_channel(
3137        &self,
3138        channel: &Channel,
3139        depth: usize,
3140        has_children: bool,
3141        is_selected: bool,
3142        ix: usize,
3143        string_match: Option<&StringMatch>,
3144        cx: &mut Context<Self>,
3145    ) -> impl IntoElement {
3146        let channel_id = channel.id;
3147
3148        let is_active = maybe!({
3149            let call_channel = ActiveCall::global(cx)
3150                .read(cx)
3151                .room()?
3152                .read(cx)
3153                .channel_id()?;
3154            Some(call_channel == channel_id)
3155        })
3156        .unwrap_or(false);
3157        let channel_store = self.channel_store.read(cx);
3158        let is_public = channel_store
3159            .channel_for_id(channel_id)
3160            .map(|channel| channel.visibility)
3161            == Some(proto::ChannelVisibility::Public);
3162        let disclosed =
3163            has_children.then(|| self.collapsed_channels.binary_search(&channel.id).is_err());
3164
3165        let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
3166
3167        const FACEPILE_LIMIT: usize = 3;
3168        let participants = self.channel_store.read(cx).channel_participants(channel_id);
3169
3170        let face_pile = if participants.is_empty() {
3171            None
3172        } else {
3173            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
3174            let result = Facepile::new(
3175                participants
3176                    .iter()
3177                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
3178                    .take(FACEPILE_LIMIT)
3179                    .chain(if extra_count > 0 {
3180                        Some(
3181                            Label::new(format!("+{extra_count}"))
3182                                .ml_2()
3183                                .into_any_element(),
3184                        )
3185                    } else {
3186                        None
3187                    })
3188                    .collect::<SmallVec<_>>(),
3189            );
3190
3191            Some(result)
3192        };
3193
3194        let width = self
3195            .workspace
3196            .read_with(cx, |workspace, cx| {
3197                workspace
3198                    .panel_size_state::<Self>(cx)
3199                    .and_then(|size_state| size_state.size)
3200            })
3201            .ok()
3202            .flatten()
3203            .unwrap_or(px(240.));
3204        let root_id = channel.root_id();
3205
3206        let is_favorited = self.is_channel_favorited(channel_id, cx);
3207        let (favorite_icon, favorite_color, favorite_tooltip) = if is_favorited {
3208            (IconName::StarFilled, Color::Accent, "Remove from Favorites")
3209        } else {
3210            (IconName::Star, Color::Default, "Add to Favorites")
3211        };
3212
3213        let height = px(24.);
3214
3215        h_flex()
3216            .id(ix)
3217            .group("")
3218            .h(height)
3219            .w_full()
3220            .overflow_hidden()
3221            .when(!channel.is_root_channel(), |el| {
3222                el.on_drag(channel.clone(), move |channel, _, _, cx| {
3223                    cx.new(|_| DraggedChannelView {
3224                        channel: channel.clone(),
3225                        width,
3226                    })
3227                })
3228            })
3229            .drag_over::<Channel>({
3230                move |style, dragged_channel: &Channel, _window, cx| {
3231                    if dragged_channel.root_id() == root_id {
3232                        style.bg(cx.theme().colors().ghost_element_hover)
3233                    } else {
3234                        style
3235                    }
3236                }
3237            })
3238            .on_drop(
3239                cx.listener(move |this, dragged_channel: &Channel, window, cx| {
3240                    if dragged_channel.root_id() != root_id {
3241                        return;
3242                    }
3243                    this.move_channel(dragged_channel.id, channel_id, window, cx);
3244                }),
3245            )
3246            .child(
3247                ListItem::new(ix)
3248                    .height(height)
3249                    // Add one level of depth for the disclosure arrow.
3250                    .indent_level(depth + 1)
3251                    .indent_step_size(px(20.))
3252                    .toggle_state(is_selected || is_active)
3253                    .toggle(disclosed)
3254                    .on_toggle(cx.listener(move |this, _, window, cx| {
3255                        this.toggle_channel_collapsed(channel_id, window, cx)
3256                    }))
3257                    .on_click(cx.listener(move |this, _, window, cx| {
3258                        if is_active {
3259                            this.open_channel_notes(channel_id, window, cx)
3260                        } else {
3261                            this.join_channel(channel_id, window, cx)
3262                        }
3263                    }))
3264                    .on_secondary_mouse_down(cx.listener(
3265                        move |this, event: &MouseDownEvent, window, cx| {
3266                            this.deploy_channel_context_menu(
3267                                event.position,
3268                                channel_id,
3269                                ix,
3270                                window,
3271                                cx,
3272                            )
3273                        },
3274                    ))
3275                    .child(
3276                        h_flex()
3277                            .id(format!("inside-{}", channel_id.0))
3278                            .w_full()
3279                            .gap_1()
3280                            .child(
3281                                div()
3282                                    .relative()
3283                                    .child(
3284                                        Icon::new(if is_public {
3285                                            IconName::Public
3286                                        } else {
3287                                            IconName::Hash
3288                                        })
3289                                        .size(IconSize::Small)
3290                                        .color(Color::Muted),
3291                                    )
3292                                    .children(has_notes_notification.then(|| {
3293                                        div()
3294                                            .w_1p5()
3295                                            .absolute()
3296                                            .right(px(-1.))
3297                                            .top(px(-1.))
3298                                            .child(Indicator::dot().color(Color::Info))
3299                                    })),
3300                            )
3301                            .child(
3302                                h_flex()
3303                                    .id(channel_id.0 as usize)
3304                                    .child(match string_match {
3305                                        None => Label::new(channel.name.clone()).into_any_element(),
3306                                        Some(string_match) => HighlightedLabel::new(
3307                                            channel.name.clone(),
3308                                            string_match.positions.clone(),
3309                                        )
3310                                        .into_any_element(),
3311                                    })
3312                                    .children(face_pile.map(|face_pile| face_pile.p_1())),
3313                            )
3314                            .tooltip({
3315                                let channel_store = self.channel_store.clone();
3316                                move |_window, cx| {
3317                                    cx.new(|_| JoinChannelTooltip {
3318                                        channel_store: channel_store.clone(),
3319                                        channel_id,
3320                                        has_notes_notification,
3321                                    })
3322                                    .into()
3323                                }
3324                            }),
3325                    ),
3326            )
3327            .child(
3328                h_flex()
3329                    .visible_on_hover("")
3330                    .h_full()
3331                    .absolute()
3332                    .right_0()
3333                    .px_1()
3334                    .gap_px()
3335                    .rounded_l_md()
3336                    .bg(cx.theme().colors().background)
3337                    .child({
3338                        let focus_handle = self.focus_handle.clone();
3339                        IconButton::new("channel_favorite", favorite_icon)
3340                            .icon_size(IconSize::Small)
3341                            .icon_color(favorite_color)
3342                            .on_click(cx.listener(move |this, _, _window, cx| {
3343                                this.toggle_favorite_channel(channel_id, cx)
3344                            }))
3345                            .tooltip(move |_window, cx| {
3346                                Tooltip::for_action_in(
3347                                    favorite_tooltip,
3348                                    &ToggleSelectedChannelFavorite,
3349                                    &focus_handle,
3350                                    cx,
3351                                )
3352                            })
3353                    })
3354                    .child({
3355                        let focus_handle = self.focus_handle.clone();
3356                        IconButton::new("channel_notes", IconName::Reader)
3357                            .icon_size(IconSize::Small)
3358                            .on_click(cx.listener(move |this, _, window, cx| {
3359                                this.open_channel_notes(channel_id, window, cx)
3360                            }))
3361                            .tooltip(move |_window, cx| {
3362                                Tooltip::for_action_in(
3363                                    "Open Channel Notes",
3364                                    &OpenSelectedChannelNotes,
3365                                    &focus_handle,
3366                                    cx,
3367                                )
3368                            })
3369                    }),
3370            )
3371    }
3372
3373    fn render_channel_editor(
3374        &self,
3375        depth: usize,
3376        _window: &mut Window,
3377        _cx: &mut Context<Self>,
3378    ) -> impl IntoElement {
3379        let item = ListItem::new("channel-editor")
3380            .inset(false)
3381            // Add one level of depth for the disclosure arrow.
3382            .indent_level(depth + 1)
3383            .indent_step_size(px(20.))
3384            .start_slot(
3385                Icon::new(IconName::Hash)
3386                    .size(IconSize::Small)
3387                    .color(Color::Muted),
3388            );
3389
3390        if let Some(pending_name) = self
3391            .channel_editing_state
3392            .as_ref()
3393            .and_then(|state| state.pending_name())
3394        {
3395            item.child(Label::new(pending_name))
3396        } else {
3397            item.child(self.channel_name_editor.clone())
3398        }
3399    }
3400}
3401
3402fn render_tree_branch(
3403    is_last: bool,
3404    overdraw: bool,
3405    window: &mut Window,
3406    cx: &mut App,
3407) -> impl IntoElement {
3408    let rem_size = window.rem_size();
3409    let line_height = window.text_style().line_height_in_pixels(rem_size);
3410    let thickness = px(1.);
3411    let color = cx.theme().colors().icon_disabled;
3412
3413    canvas(
3414        |_, _, _| {},
3415        move |bounds, _, window, _| {
3416            let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
3417            let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
3418            let right = bounds.right();
3419            let top = bounds.top();
3420
3421            window.paint_quad(fill(
3422                Bounds::from_corners(
3423                    point(start_x, top),
3424                    point(
3425                        start_x + thickness,
3426                        if is_last {
3427                            start_y
3428                        } else {
3429                            bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
3430                        },
3431                    ),
3432                ),
3433                color,
3434            ));
3435            window.paint_quad(fill(
3436                Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
3437                color,
3438            ));
3439        },
3440    )
3441    .w(rem_size)
3442    .h(line_height - px(2.))
3443}
3444
3445fn render_participant_name_and_handle(user: &User) -> impl IntoElement {
3446    Label::new(if let Some(ref display_name) = user.name {
3447        format!("{display_name} ({})", user.github_login)
3448    } else {
3449        user.github_login.to_string()
3450    })
3451}
3452
3453impl Render for CollabPanel {
3454    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3455        let status = *self.client.status().borrow();
3456
3457        v_flex()
3458            .key_context(self.dispatch_context(window, cx))
3459            .on_action(cx.listener(CollabPanel::cancel))
3460            .on_action(cx.listener(CollabPanel::select_next))
3461            .on_action(cx.listener(CollabPanel::select_previous))
3462            .on_action(cx.listener(CollabPanel::confirm))
3463            .on_action(cx.listener(CollabPanel::insert_space))
3464            .on_action(cx.listener(CollabPanel::remove_selected_channel))
3465            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
3466            .on_action(cx.listener(CollabPanel::rename_selected_channel))
3467            .on_action(cx.listener(CollabPanel::open_selected_channel_notes))
3468            .on_action(cx.listener(CollabPanel::toggle_selected_channel_favorite))
3469            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
3470            .on_action(cx.listener(CollabPanel::expand_selected_channel))
3471            .on_action(cx.listener(CollabPanel::start_move_selected_channel))
3472            .on_action(cx.listener(CollabPanel::move_channel_up))
3473            .on_action(cx.listener(CollabPanel::move_channel_down))
3474            .track_focus(&self.focus_handle)
3475            .size_full()
3476            .child(if !status.is_or_was_connected() || status.is_signing_in() {
3477                self.render_signed_out(cx)
3478            } else {
3479                self.render_signed_in(window, cx)
3480            })
3481            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3482                deferred(
3483                    anchored()
3484                        .position(*position)
3485                        .anchor(gpui::Corner::TopLeft)
3486                        .child(menu.clone()),
3487                )
3488                .with_priority(1)
3489            }))
3490    }
3491}
3492
3493impl EventEmitter<PanelEvent> for CollabPanel {}
3494
3495impl Panel for CollabPanel {
3496    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
3497        CollaborationPanelSettings::get_global(cx).dock
3498    }
3499
3500    fn position_is_valid(&self, position: DockPosition) -> bool {
3501        matches!(position, DockPosition::Left | DockPosition::Right)
3502    }
3503
3504    fn set_position(
3505        &mut self,
3506        position: DockPosition,
3507        _window: &mut Window,
3508        cx: &mut Context<Self>,
3509    ) {
3510        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
3511            settings.collaboration_panel.get_or_insert_default().dock = Some(position.into())
3512        });
3513    }
3514
3515    fn default_size(&self, _window: &Window, cx: &App) -> Pixels {
3516        CollaborationPanelSettings::get_global(cx).default_width
3517    }
3518
3519    fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
3520        CollaborationPanelSettings::get_global(cx)
3521            .button
3522            .then_some(ui::IconName::UserGroup)
3523    }
3524
3525    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
3526        Some("Collab Panel")
3527    }
3528
3529    fn toggle_action(&self) -> Box<dyn gpui::Action> {
3530        Box::new(ToggleFocus)
3531    }
3532
3533    fn persistent_name() -> &'static str {
3534        "CollabPanel"
3535    }
3536
3537    fn panel_key() -> &'static str {
3538        COLLABORATION_PANEL_KEY
3539    }
3540
3541    fn activation_priority(&self) -> u32 {
3542        5
3543    }
3544}
3545
3546impl Focusable for CollabPanel {
3547    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
3548        self.filter_editor.focus_handle(cx)
3549    }
3550}
3551
3552impl PartialEq for ListEntry {
3553    fn eq(&self, other: &Self) -> bool {
3554        match self {
3555            ListEntry::Header(section_1) => {
3556                if let ListEntry::Header(section_2) = other {
3557                    return section_1 == section_2;
3558                }
3559            }
3560            ListEntry::CallParticipant { user: user_1, .. } => {
3561                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3562                    return user_1.id == user_2.id;
3563                }
3564            }
3565            ListEntry::ParticipantProject {
3566                project_id: project_id_1,
3567                ..
3568            } => {
3569                if let ListEntry::ParticipantProject {
3570                    project_id: project_id_2,
3571                    ..
3572                } = other
3573                {
3574                    return project_id_1 == project_id_2;
3575                }
3576            }
3577            ListEntry::ParticipantScreen {
3578                peer_id: peer_id_1, ..
3579            } => {
3580                if let ListEntry::ParticipantScreen {
3581                    peer_id: peer_id_2, ..
3582                } = other
3583                {
3584                    return peer_id_1 == peer_id_2;
3585                }
3586            }
3587            ListEntry::Channel {
3588                channel: channel_1,
3589                is_favorite: is_favorite_1,
3590                ..
3591            } => {
3592                if let ListEntry::Channel {
3593                    channel: channel_2,
3594                    is_favorite: is_favorite_2,
3595                    ..
3596                } = other
3597                {
3598                    return channel_1.id == channel_2.id && is_favorite_1 == is_favorite_2;
3599                }
3600            }
3601            ListEntry::ChannelNotes { channel_id } => {
3602                if let ListEntry::ChannelNotes {
3603                    channel_id: other_id,
3604                } = other
3605                {
3606                    return channel_id == other_id;
3607                }
3608            }
3609            ListEntry::ChannelInvite(channel_1) => {
3610                if let ListEntry::ChannelInvite(channel_2) = other {
3611                    return channel_1.id == channel_2.id;
3612                }
3613            }
3614            ListEntry::IncomingRequest(user_1) => {
3615                if let ListEntry::IncomingRequest(user_2) = other {
3616                    return user_1.id == user_2.id;
3617                }
3618            }
3619            ListEntry::OutgoingRequest(user_1) => {
3620                if let ListEntry::OutgoingRequest(user_2) = other {
3621                    return user_1.id == user_2.id;
3622                }
3623            }
3624            ListEntry::Contact {
3625                contact: contact_1, ..
3626            } => {
3627                if let ListEntry::Contact {
3628                    contact: contact_2, ..
3629                } = other
3630                {
3631                    return contact_1.user.id == contact_2.user.id;
3632                }
3633            }
3634            ListEntry::ChannelEditor { depth } => {
3635                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3636                    return depth == other_depth;
3637                }
3638            }
3639            ListEntry::ContactPlaceholder => {
3640                if let ListEntry::ContactPlaceholder = other {
3641                    return true;
3642                }
3643            }
3644        }
3645        false
3646    }
3647}
3648
3649struct DraggedChannelView {
3650    channel: Channel,
3651    width: Pixels,
3652}
3653
3654impl Render for DraggedChannelView {
3655    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3656        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3657        h_flex()
3658            .font_family(ui_font)
3659            .bg(cx.theme().colors().background)
3660            .w(self.width)
3661            .p_1()
3662            .gap_1()
3663            .child(
3664                Icon::new(
3665                    if self.channel.visibility == proto::ChannelVisibility::Public {
3666                        IconName::Public
3667                    } else {
3668                        IconName::Hash
3669                    },
3670                )
3671                .size(IconSize::Small)
3672                .color(Color::Muted),
3673            )
3674            .child(Label::new(self.channel.name.clone()))
3675    }
3676}
3677
3678struct JoinChannelTooltip {
3679    channel_store: Entity<ChannelStore>,
3680    channel_id: ChannelId,
3681    #[allow(unused)]
3682    has_notes_notification: bool,
3683}
3684
3685impl Render for JoinChannelTooltip {
3686    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3687        tooltip_container(cx, |container, cx| {
3688            let participants = self
3689                .channel_store
3690                .read(cx)
3691                .channel_participants(self.channel_id);
3692
3693            container
3694                .child(Label::new("Join Channel"))
3695                .children(participants.iter().map(|participant| {
3696                    h_flex()
3697                        .gap_2()
3698                        .child(Avatar::new(participant.avatar_uri.clone()))
3699                        .child(render_participant_name_and_handle(participant))
3700                }))
3701        })
3702    }
3703}
3704
3705#[cfg(any(test, feature = "test-support"))]
3706impl CollabPanel {
3707    pub fn entries_as_strings(&self) -> Vec<String> {
3708        let mut string_entries = Vec::new();
3709        for (index, entry) in self.entries.iter().enumerate() {
3710            let selected_marker = if self.selection == Some(index) {
3711                "  <== selected"
3712            } else {
3713                ""
3714            };
3715            match entry {
3716                ListEntry::Header(section) => {
3717                    let name = match section {
3718                        Section::ActiveCall => "Active Call",
3719                        Section::FavoriteChannels => "Favorites",
3720                        Section::Channels => "Channels",
3721                        Section::ChannelInvites => "Channel Invites",
3722                        Section::ContactRequests => "Contact Requests",
3723                        Section::Contacts => "Contacts",
3724                        Section::Online => "Online",
3725                        Section::Offline => "Offline",
3726                    };
3727                    string_entries.push(format!("[{name}]"));
3728                }
3729                ListEntry::Channel {
3730                    channel,
3731                    depth,
3732                    has_children,
3733                    ..
3734                } => {
3735                    let indent = "  ".repeat(*depth + 1);
3736                    let icon = if *has_children {
3737                        "v "
3738                    } else if channel.visibility == proto::ChannelVisibility::Public {
3739                        "🛜 "
3740                    } else {
3741                        "#️⃣ "
3742                    };
3743                    string_entries.push(format!("{indent}{icon}{}{selected_marker}", channel.name));
3744                }
3745                ListEntry::ChannelNotes { .. } => {
3746                    string_entries.push(format!("  (notes){selected_marker}"));
3747                }
3748                ListEntry::ChannelEditor { depth } => {
3749                    let indent = "  ".repeat(*depth + 1);
3750                    string_entries.push(format!("{indent}[editor]{selected_marker}"));
3751                }
3752                ListEntry::ChannelInvite(channel) => {
3753                    string_entries.push(format!("  (invite) #{}{selected_marker}", channel.name));
3754                }
3755                ListEntry::CallParticipant { user, .. } => {
3756                    string_entries.push(format!("  {}{selected_marker}", user.github_login));
3757                }
3758                ListEntry::ParticipantProject {
3759                    worktree_root_names,
3760                    ..
3761                } => {
3762                    string_entries.push(format!(
3763                        "    {}{selected_marker}",
3764                        worktree_root_names.join(", ")
3765                    ));
3766                }
3767                ListEntry::ParticipantScreen { .. } => {
3768                    string_entries.push(format!("    (screen){selected_marker}"));
3769                }
3770                ListEntry::IncomingRequest(user) => {
3771                    string_entries.push(format!(
3772                        "  (incoming) {}{selected_marker}",
3773                        user.github_login
3774                    ));
3775                }
3776                ListEntry::OutgoingRequest(user) => {
3777                    string_entries.push(format!(
3778                        "  (outgoing) {}{selected_marker}",
3779                        user.github_login
3780                    ));
3781                }
3782                ListEntry::Contact { contact, .. } => {
3783                    string_entries
3784                        .push(format!("  {}{selected_marker}", contact.user.github_login));
3785                }
3786                ListEntry::ContactPlaceholder => {}
3787            }
3788        }
3789        string_entries
3790    }
3791}