collab_panel.rs

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