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