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