collab_panel.rs

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