collab_panel.rs

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