collab_panel.rs

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