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