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