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