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