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