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