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