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