collab_panel.rs

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