collab_panel.rs

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