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 cx.stop_active_drag(window) {
1467            return;
1468        } else if self.take_editing_state(window, cx) {
1469            window.focus(&self.filter_editor.focus_handle(cx));
1470        } else if !self.reset_filter_editor_text(window, cx) {
1471            self.focus_handle.focus(window);
1472        }
1473
1474        if self.context_menu.is_some() {
1475            self.context_menu.take();
1476            cx.notify();
1477        }
1478
1479        self.update_entries(false, cx);
1480    }
1481
1482    fn select_next(&mut self, _: &SelectNext, _: &mut Window, cx: &mut Context<Self>) {
1483        let ix = self.selection.map_or(0, |ix| ix + 1);
1484        if ix < self.entries.len() {
1485            self.selection = Some(ix);
1486        }
1487
1488        if let Some(ix) = self.selection {
1489            self.scroll_to_item(ix)
1490        }
1491        cx.notify();
1492    }
1493
1494    fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
1495        let ix = self.selection.take().unwrap_or(0);
1496        if ix > 0 {
1497            self.selection = Some(ix - 1);
1498        }
1499
1500        if let Some(ix) = self.selection {
1501            self.scroll_to_item(ix)
1502        }
1503        cx.notify();
1504    }
1505
1506    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1507        if self.confirm_channel_edit(window, cx) {
1508            return;
1509        }
1510
1511        if let Some(selection) = self.selection {
1512            if let Some(entry) = self.entries.get(selection) {
1513                match entry {
1514                    ListEntry::Header(section) => match section {
1515                        Section::ActiveCall => Self::leave_call(window, cx),
1516                        Section::Channels => self.new_root_channel(window, cx),
1517                        Section::Contacts => self.toggle_contact_finder(window, cx),
1518                        Section::ContactRequests
1519                        | Section::Online
1520                        | Section::Offline
1521                        | Section::ChannelInvites => {
1522                            self.toggle_section_expanded(*section, cx);
1523                        }
1524                    },
1525                    ListEntry::Contact { contact, calling } => {
1526                        if contact.online && !contact.busy && !calling {
1527                            self.call(contact.user.id, window, cx);
1528                        }
1529                    }
1530                    ListEntry::ParticipantProject {
1531                        project_id,
1532                        host_user_id,
1533                        ..
1534                    } => {
1535                        if let Some(workspace) = self.workspace.upgrade() {
1536                            let app_state = workspace.read(cx).app_state().clone();
1537                            workspace::join_in_room_project(
1538                                *project_id,
1539                                *host_user_id,
1540                                app_state,
1541                                cx,
1542                            )
1543                            .detach_and_prompt_err(
1544                                "Failed to join project",
1545                                window,
1546                                cx,
1547                                |_, _, _| None,
1548                            );
1549                        }
1550                    }
1551                    ListEntry::ParticipantScreen { peer_id, .. } => {
1552                        let Some(peer_id) = peer_id else {
1553                            return;
1554                        };
1555                        if let Some(workspace) = self.workspace.upgrade() {
1556                            workspace.update(cx, |workspace, cx| {
1557                                workspace.open_shared_screen(*peer_id, window, cx)
1558                            });
1559                        }
1560                    }
1561                    ListEntry::Channel { channel, .. } => {
1562                        let is_active = maybe!({
1563                            let call_channel = ActiveCall::global(cx)
1564                                .read(cx)
1565                                .room()?
1566                                .read(cx)
1567                                .channel_id()?;
1568
1569                            Some(call_channel == channel.id)
1570                        })
1571                        .unwrap_or(false);
1572                        if is_active {
1573                            self.open_channel_notes(channel.id, window, cx)
1574                        } else {
1575                            self.join_channel(channel.id, window, cx)
1576                        }
1577                    }
1578                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx),
1579                    ListEntry::CallParticipant { user, peer_id, .. } => {
1580                        if Some(user) == self.user_store.read(cx).current_user().as_ref() {
1581                            Self::leave_call(window, cx);
1582                        } else if let Some(peer_id) = peer_id {
1583                            self.workspace
1584                                .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx))
1585                                .ok();
1586                        }
1587                    }
1588                    ListEntry::IncomingRequest(user) => {
1589                        self.respond_to_contact_request(user.id, true, window, cx)
1590                    }
1591                    ListEntry::ChannelInvite(channel) => {
1592                        self.respond_to_channel_invite(channel.id, true, cx)
1593                    }
1594                    ListEntry::ChannelNotes { channel_id } => {
1595                        self.open_channel_notes(*channel_id, window, cx)
1596                    }
1597                    ListEntry::ChannelChat { channel_id } => {
1598                        self.join_channel_chat(*channel_id, window, cx)
1599                    }
1600                    ListEntry::OutgoingRequest(_) => {}
1601                    ListEntry::ChannelEditor { .. } => {}
1602                }
1603            }
1604        }
1605    }
1606
1607    fn insert_space(&mut self, _: &InsertSpace, window: &mut Window, cx: &mut Context<Self>) {
1608        if self.channel_editing_state.is_some() {
1609            self.channel_name_editor.update(cx, |editor, cx| {
1610                editor.insert(" ", window, cx);
1611            });
1612        }
1613    }
1614
1615    fn confirm_channel_edit(&mut self, window: &mut Window, cx: &mut Context<CollabPanel>) -> bool {
1616        if let Some(editing_state) = &mut self.channel_editing_state {
1617            match editing_state {
1618                ChannelEditingState::Create {
1619                    location,
1620                    pending_name,
1621                    ..
1622                } => {
1623                    if pending_name.is_some() {
1624                        return false;
1625                    }
1626                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1627
1628                    *pending_name = Some(channel_name.clone());
1629
1630                    let create = self.channel_store.update(cx, |channel_store, cx| {
1631                        channel_store.create_channel(&channel_name, *location, cx)
1632                    });
1633                    if location.is_none() {
1634                        cx.spawn_in(window, async move |this, cx| {
1635                            let channel_id = create.await?;
1636                            this.update_in(cx, |this, window, cx| {
1637                                this.show_channel_modal(
1638                                    channel_id,
1639                                    channel_modal::Mode::InviteMembers,
1640                                    window,
1641                                    cx,
1642                                )
1643                            })
1644                        })
1645                        .detach_and_prompt_err(
1646                            "Failed to create channel",
1647                            window,
1648                            cx,
1649                            |_, _, _| None,
1650                        );
1651                    } else {
1652                        create.detach_and_prompt_err(
1653                            "Failed to create channel",
1654                            window,
1655                            cx,
1656                            |_, _, _| None,
1657                        );
1658                    }
1659                    cx.notify();
1660                }
1661                ChannelEditingState::Rename {
1662                    location,
1663                    pending_name,
1664                } => {
1665                    if pending_name.is_some() {
1666                        return false;
1667                    }
1668                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1669                    *pending_name = Some(channel_name.clone());
1670
1671                    self.channel_store
1672                        .update(cx, |channel_store, cx| {
1673                            channel_store.rename(*location, &channel_name, cx)
1674                        })
1675                        .detach();
1676                    cx.notify();
1677                }
1678            }
1679            cx.focus_self(window);
1680            true
1681        } else {
1682            false
1683        }
1684    }
1685
1686    fn toggle_section_expanded(&mut self, section: Section, cx: &mut Context<Self>) {
1687        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1688            self.collapsed_sections.remove(ix);
1689        } else {
1690            self.collapsed_sections.push(section);
1691        }
1692        self.update_entries(false, cx);
1693    }
1694
1695    fn collapse_selected_channel(
1696        &mut self,
1697        _: &CollapseSelectedChannel,
1698        window: &mut Window,
1699        cx: &mut Context<Self>,
1700    ) {
1701        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1702            return;
1703        };
1704
1705        if self.is_channel_collapsed(channel_id) {
1706            return;
1707        }
1708
1709        self.toggle_channel_collapsed(channel_id, window, cx);
1710    }
1711
1712    fn expand_selected_channel(
1713        &mut self,
1714        _: &ExpandSelectedChannel,
1715        window: &mut Window,
1716        cx: &mut Context<Self>,
1717    ) {
1718        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1719            return;
1720        };
1721
1722        if !self.is_channel_collapsed(id) {
1723            return;
1724        }
1725
1726        self.toggle_channel_collapsed(id, window, cx)
1727    }
1728
1729    fn toggle_channel_collapsed(
1730        &mut self,
1731        channel_id: ChannelId,
1732        window: &mut Window,
1733        cx: &mut Context<Self>,
1734    ) {
1735        match self.collapsed_channels.binary_search(&channel_id) {
1736            Ok(ix) => {
1737                self.collapsed_channels.remove(ix);
1738            }
1739            Err(ix) => {
1740                self.collapsed_channels.insert(ix, channel_id);
1741            }
1742        };
1743        self.serialize(cx);
1744        self.update_entries(true, cx);
1745        cx.notify();
1746        cx.focus_self(window);
1747    }
1748
1749    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1750        self.collapsed_channels.binary_search(&channel_id).is_ok()
1751    }
1752
1753    fn leave_call(window: &mut Window, cx: &mut App) {
1754        ActiveCall::global(cx)
1755            .update(cx, |call, cx| call.hang_up(cx))
1756            .detach_and_prompt_err("Failed to hang up", window, cx, |_, _, _| None);
1757    }
1758
1759    fn toggle_contact_finder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1760        if let Some(workspace) = self.workspace.upgrade() {
1761            workspace.update(cx, |workspace, cx| {
1762                workspace.toggle_modal(window, cx, |window, cx| {
1763                    let mut finder = ContactFinder::new(self.user_store.clone(), window, cx);
1764                    finder.set_query(self.filter_editor.read(cx).text(cx), window, cx);
1765                    finder
1766                });
1767            });
1768        }
1769    }
1770
1771    fn new_root_channel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1772        self.channel_editing_state = Some(ChannelEditingState::Create {
1773            location: None,
1774            pending_name: None,
1775        });
1776        self.update_entries(false, cx);
1777        self.select_channel_editor();
1778        window.focus(&self.channel_name_editor.focus_handle(cx));
1779        cx.notify();
1780    }
1781
1782    fn select_channel_editor(&mut self) {
1783        self.selection = self.entries.iter().position(|entry| match entry {
1784            ListEntry::ChannelEditor { .. } => true,
1785            _ => false,
1786        });
1787    }
1788
1789    fn new_subchannel(
1790        &mut self,
1791        channel_id: ChannelId,
1792        window: &mut Window,
1793        cx: &mut Context<Self>,
1794    ) {
1795        self.collapsed_channels
1796            .retain(|channel| *channel != channel_id);
1797        self.channel_editing_state = Some(ChannelEditingState::Create {
1798            location: Some(channel_id),
1799            pending_name: None,
1800        });
1801        self.update_entries(false, cx);
1802        self.select_channel_editor();
1803        window.focus(&self.channel_name_editor.focus_handle(cx));
1804        cx.notify();
1805    }
1806
1807    fn manage_members(
1808        &mut self,
1809        channel_id: ChannelId,
1810        window: &mut Window,
1811        cx: &mut Context<Self>,
1812    ) {
1813        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, window, cx);
1814    }
1815
1816    fn remove_selected_channel(&mut self, _: &Remove, window: &mut Window, cx: &mut Context<Self>) {
1817        if let Some(channel) = self.selected_channel() {
1818            self.remove_channel(channel.id, window, cx)
1819        }
1820    }
1821
1822    fn rename_selected_channel(
1823        &mut self,
1824        _: &SecondaryConfirm,
1825        window: &mut Window,
1826        cx: &mut Context<Self>,
1827    ) {
1828        if let Some(channel) = self.selected_channel() {
1829            self.rename_channel(channel.id, window, cx);
1830        }
1831    }
1832
1833    fn rename_channel(
1834        &mut self,
1835        channel_id: ChannelId,
1836        window: &mut Window,
1837        cx: &mut Context<Self>,
1838    ) {
1839        let channel_store = self.channel_store.read(cx);
1840        if !channel_store.is_channel_admin(channel_id) {
1841            return;
1842        }
1843        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
1844            self.channel_editing_state = Some(ChannelEditingState::Rename {
1845                location: channel_id,
1846                pending_name: None,
1847            });
1848            self.channel_name_editor.update(cx, |editor, cx| {
1849                editor.set_text(channel.name.clone(), window, cx);
1850                editor.select_all(&Default::default(), window, cx);
1851            });
1852            window.focus(&self.channel_name_editor.focus_handle(cx));
1853            self.update_entries(false, cx);
1854            self.select_channel_editor();
1855        }
1856    }
1857
1858    fn set_channel_visibility(
1859        &mut self,
1860        channel_id: ChannelId,
1861        visibility: ChannelVisibility,
1862        window: &mut Window,
1863        cx: &mut Context<Self>,
1864    ) {
1865        self.channel_store
1866            .update(cx, |channel_store, cx| {
1867                channel_store.set_channel_visibility(channel_id, visibility, cx)
1868            })
1869            .detach_and_prompt_err("Failed to set channel visibility", window, cx, |e, _, _| match e.error_code() {
1870                ErrorCode::BadPublicNesting =>
1871                    if e.error_tag("direction") == Some("parent") {
1872                        Some("To make a channel public, its parent channel must be public.".to_string())
1873                    } else {
1874                        Some("To make a channel private, all of its subchannels must be private.".to_string())
1875                    },
1876                _ => None
1877            });
1878    }
1879
1880    fn start_move_channel(
1881        &mut self,
1882        channel_id: ChannelId,
1883        _window: &mut Window,
1884        _cx: &mut Context<Self>,
1885    ) {
1886        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
1887    }
1888
1889    fn start_move_selected_channel(
1890        &mut self,
1891        _: &StartMoveChannel,
1892        window: &mut Window,
1893        cx: &mut Context<Self>,
1894    ) {
1895        if let Some(channel) = self.selected_channel() {
1896            self.start_move_channel(channel.id, window, cx);
1897        }
1898    }
1899
1900    fn move_channel_on_clipboard(
1901        &mut self,
1902        to_channel_id: ChannelId,
1903        window: &mut Window,
1904        cx: &mut Context<CollabPanel>,
1905    ) {
1906        if let Some(clipboard) = self.channel_clipboard.take() {
1907            self.move_channel(clipboard.channel_id, to_channel_id, window, cx)
1908        }
1909    }
1910
1911    fn move_channel(
1912        &self,
1913        channel_id: ChannelId,
1914        to: ChannelId,
1915        window: &mut Window,
1916        cx: &mut Context<Self>,
1917    ) {
1918        self.channel_store
1919            .update(cx, |channel_store, cx| {
1920                channel_store.move_channel(channel_id, to, cx)
1921            })
1922            .detach_and_prompt_err("Failed to move channel", window, cx, |e, _, _| {
1923                match e.error_code() {
1924                    ErrorCode::BadPublicNesting => {
1925                        Some("Public channels must have public parents".into())
1926                    }
1927                    ErrorCode::CircularNesting => {
1928                        Some("You cannot move a channel into itself".into())
1929                    }
1930                    ErrorCode::WrongMoveTarget => {
1931                        Some("You cannot move a channel into a different root channel".into())
1932                    }
1933                    _ => None,
1934                }
1935            })
1936    }
1937
1938    fn open_channel_notes(
1939        &mut self,
1940        channel_id: ChannelId,
1941        window: &mut Window,
1942        cx: &mut Context<Self>,
1943    ) {
1944        if let Some(workspace) = self.workspace.upgrade() {
1945            ChannelView::open(channel_id, None, workspace, window, cx).detach();
1946        }
1947    }
1948
1949    fn show_inline_context_menu(
1950        &mut self,
1951        _: &menu::SecondaryConfirm,
1952        window: &mut Window,
1953        cx: &mut Context<Self>,
1954    ) {
1955        let Some(bounds) = self
1956            .selection
1957            .and_then(|ix| self.list_state.bounds_for_item(ix))
1958        else {
1959            return;
1960        };
1961
1962        if let Some(channel) = self.selected_channel() {
1963            self.deploy_channel_context_menu(
1964                bounds.center(),
1965                channel.id,
1966                self.selection.unwrap(),
1967                window,
1968                cx,
1969            );
1970            cx.stop_propagation();
1971            return;
1972        };
1973
1974        if let Some(contact) = self.selected_contact() {
1975            self.deploy_contact_context_menu(bounds.center(), contact, window, cx);
1976            cx.stop_propagation();
1977        }
1978    }
1979
1980    fn selected_channel(&self) -> Option<&Arc<Channel>> {
1981        self.selection
1982            .and_then(|ix| self.entries.get(ix))
1983            .and_then(|entry| match entry {
1984                ListEntry::Channel { channel, .. } => Some(channel),
1985                _ => None,
1986            })
1987    }
1988
1989    fn selected_contact(&self) -> Option<Arc<Contact>> {
1990        self.selection
1991            .and_then(|ix| self.entries.get(ix))
1992            .and_then(|entry| match entry {
1993                ListEntry::Contact { contact, .. } => Some(contact.clone()),
1994                _ => None,
1995            })
1996    }
1997
1998    fn show_channel_modal(
1999        &mut self,
2000        channel_id: ChannelId,
2001        mode: channel_modal::Mode,
2002        window: &mut Window,
2003        cx: &mut Context<Self>,
2004    ) {
2005        let workspace = self.workspace.clone();
2006        let user_store = self.user_store.clone();
2007        let channel_store = self.channel_store.clone();
2008
2009        cx.spawn_in(window, async move |_, cx| {
2010            workspace.update_in(cx, |workspace, window, cx| {
2011                workspace.toggle_modal(window, cx, |window, cx| {
2012                    ChannelModal::new(
2013                        user_store.clone(),
2014                        channel_store.clone(),
2015                        channel_id,
2016                        mode,
2017                        window,
2018                        cx,
2019                    )
2020                });
2021            })
2022        })
2023        .detach();
2024    }
2025
2026    fn leave_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
2027        let Some(user_id) = self.user_store.read(cx).current_user().map(|u| u.id) else {
2028            return;
2029        };
2030        let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) else {
2031            return;
2032        };
2033        let prompt_message = format!("Are you sure you want to leave \"#{}\"?", channel.name);
2034        let answer = window.prompt(
2035            PromptLevel::Warning,
2036            &prompt_message,
2037            None,
2038            &["Leave", "Cancel"],
2039            cx,
2040        );
2041        cx.spawn_in(window, async move |this, cx| {
2042            if answer.await? != 0 {
2043                return Ok(());
2044            }
2045            this.update(cx, |this, cx| {
2046                this.channel_store.update(cx, |channel_store, cx| {
2047                    channel_store.remove_member(channel_id, user_id, cx)
2048                })
2049            })?
2050            .await
2051        })
2052        .detach_and_prompt_err("Failed to leave channel", window, cx, |_, _, _| None)
2053    }
2054
2055    fn remove_channel(
2056        &mut self,
2057        channel_id: ChannelId,
2058        window: &mut Window,
2059        cx: &mut Context<Self>,
2060    ) {
2061        let channel_store = self.channel_store.clone();
2062        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2063            let prompt_message = format!(
2064                "Are you sure you want to remove the channel \"{}\"?",
2065                channel.name
2066            );
2067            let answer = window.prompt(
2068                PromptLevel::Warning,
2069                &prompt_message,
2070                None,
2071                &["Remove", "Cancel"],
2072                cx,
2073            );
2074            cx.spawn_in(window, async move |this, cx| {
2075                if answer.await? == 0 {
2076                    channel_store
2077                        .update(cx, |channels, _| channels.remove_channel(channel_id))?
2078                        .await
2079                        .notify_async_err(cx);
2080                    this.update_in(cx, |_, window, cx| cx.focus_self(window))
2081                        .ok();
2082                }
2083                anyhow::Ok(())
2084            })
2085            .detach();
2086        }
2087    }
2088
2089    fn remove_contact(
2090        &mut self,
2091        user_id: u64,
2092        github_login: &str,
2093        window: &mut Window,
2094        cx: &mut Context<Self>,
2095    ) {
2096        let user_store = self.user_store.clone();
2097        let prompt_message = format!(
2098            "Are you sure you want to remove \"{}\" from your contacts?",
2099            github_login
2100        );
2101        let answer = window.prompt(
2102            PromptLevel::Warning,
2103            &prompt_message,
2104            None,
2105            &["Remove", "Cancel"],
2106            cx,
2107        );
2108        cx.spawn_in(window, async move |_, cx| {
2109            if answer.await? == 0 {
2110                user_store
2111                    .update(cx, |store, cx| store.remove_contact(user_id, cx))?
2112                    .await
2113                    .notify_async_err(cx);
2114            }
2115            anyhow::Ok(())
2116        })
2117        .detach_and_prompt_err("Failed to remove contact", window, cx, |_, _, _| None);
2118    }
2119
2120    fn respond_to_contact_request(
2121        &mut self,
2122        user_id: u64,
2123        accept: bool,
2124        window: &mut Window,
2125        cx: &mut Context<Self>,
2126    ) {
2127        self.user_store
2128            .update(cx, |store, cx| {
2129                store.respond_to_contact_request(user_id, accept, cx)
2130            })
2131            .detach_and_prompt_err(
2132                "Failed to respond to contact request",
2133                window,
2134                cx,
2135                |_, _, _| None,
2136            );
2137    }
2138
2139    fn respond_to_channel_invite(
2140        &mut self,
2141        channel_id: ChannelId,
2142        accept: bool,
2143        cx: &mut Context<Self>,
2144    ) {
2145        self.channel_store
2146            .update(cx, |store, cx| {
2147                store.respond_to_channel_invite(channel_id, accept, cx)
2148            })
2149            .detach();
2150    }
2151
2152    fn call(&mut self, recipient_user_id: u64, window: &mut Window, cx: &mut Context<Self>) {
2153        ActiveCall::global(cx)
2154            .update(cx, |call, cx| {
2155                call.invite(recipient_user_id, Some(self.project.clone()), cx)
2156            })
2157            .detach_and_prompt_err("Call failed", window, cx, |_, _, _| None);
2158    }
2159
2160    fn join_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
2161        let Some(workspace) = self.workspace.upgrade() else {
2162            return;
2163        };
2164        let Some(handle) = window.window_handle().downcast::<Workspace>() else {
2165            return;
2166        };
2167        workspace::join_channel(
2168            channel_id,
2169            workspace.read(cx).app_state().clone(),
2170            Some(handle),
2171            cx,
2172        )
2173        .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None)
2174    }
2175
2176    fn join_channel_chat(
2177        &mut self,
2178        channel_id: ChannelId,
2179        window: &mut Window,
2180        cx: &mut Context<Self>,
2181    ) {
2182        let Some(workspace) = self.workspace.upgrade() else {
2183            return;
2184        };
2185        window.defer(cx, move |window, cx| {
2186            workspace.update(cx, |workspace, cx| {
2187                if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {
2188                    panel.update(cx, |panel, cx| {
2189                        panel
2190                            .select_channel(channel_id, None, cx)
2191                            .detach_and_notify_err(window, cx);
2192                    });
2193                }
2194            });
2195        });
2196    }
2197
2198    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
2199        let channel_store = self.channel_store.read(cx);
2200        let Some(channel) = channel_store.channel_for_id(channel_id) else {
2201            return;
2202        };
2203        let item = ClipboardItem::new_string(channel.link(cx));
2204        cx.write_to_clipboard(item)
2205    }
2206
2207    fn render_signed_out(&mut self, cx: &mut Context<Self>) -> Div {
2208        let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
2209
2210        v_flex()
2211            .gap_6()
2212            .p_4()
2213            .child(Label::new(collab_blurb))
2214            .child(
2215                v_flex()
2216                    .gap_2()
2217                    .child(
2218                        Button::new("sign_in", "Sign in")
2219                            .icon_color(Color::Muted)
2220                            .icon(IconName::Github)
2221                            .icon_position(IconPosition::Start)
2222                            .style(ButtonStyle::Filled)
2223                            .full_width()
2224                            .on_click(cx.listener(|this, _, window, cx| {
2225                                let client = this.client.clone();
2226                                cx.spawn_in(window, async move |_, cx| {
2227                                    client
2228                                        .authenticate_and_connect(true, &cx)
2229                                        .await
2230                                        .into_response()
2231                                        .notify_async_err(cx);
2232                                })
2233                                .detach()
2234                            })),
2235                    )
2236                    .child(
2237                        div().flex().w_full().items_center().child(
2238                            Label::new("Sign in to enable collaboration.")
2239                                .color(Color::Muted)
2240                                .size(LabelSize::Small),
2241                        ),
2242                    ),
2243            )
2244    }
2245
2246    fn render_list_entry(
2247        &mut self,
2248        ix: usize,
2249        window: &mut Window,
2250        cx: &mut Context<Self>,
2251    ) -> AnyElement {
2252        let entry = &self.entries[ix];
2253
2254        let is_selected = self.selection == Some(ix);
2255        match entry {
2256            ListEntry::Header(section) => {
2257                let is_collapsed = self.collapsed_sections.contains(section);
2258                self.render_header(*section, is_selected, is_collapsed, cx)
2259                    .into_any_element()
2260            }
2261            ListEntry::Contact { contact, calling } => self
2262                .render_contact(contact, *calling, is_selected, cx)
2263                .into_any_element(),
2264            ListEntry::ContactPlaceholder => self
2265                .render_contact_placeholder(is_selected, cx)
2266                .into_any_element(),
2267            ListEntry::IncomingRequest(user) => self
2268                .render_contact_request(user, true, is_selected, cx)
2269                .into_any_element(),
2270            ListEntry::OutgoingRequest(user) => self
2271                .render_contact_request(user, false, is_selected, cx)
2272                .into_any_element(),
2273            ListEntry::Channel {
2274                channel,
2275                depth,
2276                has_children,
2277            } => self
2278                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
2279                .into_any_element(),
2280            ListEntry::ChannelEditor { depth } => self
2281                .render_channel_editor(*depth, window, cx)
2282                .into_any_element(),
2283            ListEntry::ChannelInvite(channel) => self
2284                .render_channel_invite(channel, is_selected, cx)
2285                .into_any_element(),
2286            ListEntry::CallParticipant {
2287                user,
2288                peer_id,
2289                is_pending,
2290                role,
2291            } => self
2292                .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
2293                .into_any_element(),
2294            ListEntry::ParticipantProject {
2295                project_id,
2296                worktree_root_names,
2297                host_user_id,
2298                is_last,
2299            } => self
2300                .render_participant_project(
2301                    *project_id,
2302                    worktree_root_names,
2303                    *host_user_id,
2304                    *is_last,
2305                    is_selected,
2306                    window,
2307                    cx,
2308                )
2309                .into_any_element(),
2310            ListEntry::ParticipantScreen { peer_id, is_last } => self
2311                .render_participant_screen(*peer_id, *is_last, is_selected, window, cx)
2312                .into_any_element(),
2313            ListEntry::ChannelNotes { channel_id } => self
2314                .render_channel_notes(*channel_id, is_selected, window, cx)
2315                .into_any_element(),
2316            ListEntry::ChannelChat { channel_id } => self
2317                .render_channel_chat(*channel_id, is_selected, window, cx)
2318                .into_any_element(),
2319        }
2320    }
2321
2322    fn render_signed_in(&mut self, _: &mut Window, cx: &mut Context<Self>) -> Div {
2323        self.channel_store.update(cx, |channel_store, _| {
2324            channel_store.initialize();
2325        });
2326        v_flex()
2327            .size_full()
2328            .child(list(self.list_state.clone()).size_full())
2329            .child(
2330                v_flex()
2331                    .child(div().mx_2().border_primary(cx).border_t_1())
2332                    .child(
2333                        v_flex()
2334                            .p_2()
2335                            .child(self.render_filter_input(&self.filter_editor, cx)),
2336                    ),
2337            )
2338    }
2339
2340    fn render_filter_input(
2341        &self,
2342        editor: &Entity<Editor>,
2343        cx: &mut Context<Self>,
2344    ) -> impl IntoElement {
2345        let settings = ThemeSettings::get_global(cx);
2346        let text_style = TextStyle {
2347            color: if editor.read(cx).read_only(cx) {
2348                cx.theme().colors().text_disabled
2349            } else {
2350                cx.theme().colors().text
2351            },
2352            font_family: settings.ui_font.family.clone(),
2353            font_features: settings.ui_font.features.clone(),
2354            font_fallbacks: settings.ui_font.fallbacks.clone(),
2355            font_size: rems(0.875).into(),
2356            font_weight: settings.ui_font.weight,
2357            font_style: FontStyle::Normal,
2358            line_height: relative(1.3),
2359            ..Default::default()
2360        };
2361
2362        EditorElement::new(
2363            editor,
2364            EditorStyle {
2365                local_player: cx.theme().players().local(),
2366                text: text_style,
2367                ..Default::default()
2368            },
2369        )
2370    }
2371
2372    fn render_header(
2373        &self,
2374        section: Section,
2375        is_selected: bool,
2376        is_collapsed: bool,
2377        cx: &mut Context<Self>,
2378    ) -> impl IntoElement {
2379        let mut channel_link = None;
2380        let mut channel_tooltip_text = None;
2381        let mut channel_icon = None;
2382
2383        let text = match section {
2384            Section::ActiveCall => {
2385                let channel_name = maybe!({
2386                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2387
2388                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2389
2390                    channel_link = Some(channel.link(cx));
2391                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2392                        proto::ChannelVisibility::Public => {
2393                            (Some("icons/public.svg"), Some("Copy public channel link."))
2394                        }
2395                        proto::ChannelVisibility::Members => {
2396                            (Some("icons/hash.svg"), Some("Copy private channel link."))
2397                        }
2398                    };
2399
2400                    Some(channel.name.as_ref())
2401                });
2402
2403                if let Some(name) = channel_name {
2404                    SharedString::from(name.to_string())
2405                } else {
2406                    SharedString::from("Current Call")
2407                }
2408            }
2409            Section::ContactRequests => SharedString::from("Requests"),
2410            Section::Contacts => SharedString::from("Contacts"),
2411            Section::Channels => SharedString::from("Channels"),
2412            Section::ChannelInvites => SharedString::from("Invites"),
2413            Section::Online => SharedString::from("Online"),
2414            Section::Offline => SharedString::from("Offline"),
2415        };
2416
2417        let button = match section {
2418            Section::ActiveCall => channel_link.map(|channel_link| {
2419                let channel_link_copy = channel_link.clone();
2420                IconButton::new("channel-link", IconName::Copy)
2421                    .icon_size(IconSize::Small)
2422                    .size(ButtonSize::None)
2423                    .visible_on_hover("section-header")
2424                    .on_click(move |_, _, cx| {
2425                        let item = ClipboardItem::new_string(channel_link_copy.clone());
2426                        cx.write_to_clipboard(item)
2427                    })
2428                    .tooltip(Tooltip::text("Copy channel link"))
2429                    .into_any_element()
2430            }),
2431            Section::Contacts => Some(
2432                IconButton::new("add-contact", IconName::Plus)
2433                    .on_click(
2434                        cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)),
2435                    )
2436                    .tooltip(Tooltip::text("Search for new contact"))
2437                    .into_any_element(),
2438            ),
2439            Section::Channels => Some(
2440                IconButton::new("add-channel", IconName::Plus)
2441                    .on_click(cx.listener(|this, _, window, cx| this.new_root_channel(window, cx)))
2442                    .tooltip(Tooltip::text("Create a channel"))
2443                    .into_any_element(),
2444            ),
2445            _ => None,
2446        };
2447
2448        let can_collapse = match section {
2449            Section::ActiveCall | Section::Channels | Section::Contacts => false,
2450            Section::ChannelInvites
2451            | Section::ContactRequests
2452            | Section::Online
2453            | Section::Offline => true,
2454        };
2455
2456        h_flex().w_full().group("section-header").child(
2457            ListHeader::new(text)
2458                .when(can_collapse, |header| {
2459                    header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
2460                        move |this, _, _, cx| {
2461                            this.toggle_section_expanded(section, cx);
2462                        },
2463                    ))
2464                })
2465                .inset(true)
2466                .end_slot::<AnyElement>(button)
2467                .toggle_state(is_selected),
2468        )
2469    }
2470
2471    fn render_contact(
2472        &self,
2473        contact: &Arc<Contact>,
2474        calling: bool,
2475        is_selected: bool,
2476        cx: &mut Context<Self>,
2477    ) -> impl IntoElement {
2478        let online = contact.online;
2479        let busy = contact.busy || calling;
2480        let github_login = SharedString::from(contact.user.github_login.clone());
2481        let item = ListItem::new(github_login.clone())
2482            .indent_level(1)
2483            .indent_step_size(px(20.))
2484            .toggle_state(is_selected)
2485            .child(
2486                h_flex()
2487                    .w_full()
2488                    .justify_between()
2489                    .child(Label::new(github_login.clone()))
2490                    .when(calling, |el| {
2491                        el.child(Label::new("Calling").color(Color::Muted))
2492                    })
2493                    .when(!calling, |el| {
2494                        el.child(
2495                            IconButton::new("contact context menu", IconName::Ellipsis)
2496                                .icon_color(Color::Muted)
2497                                .visible_on_hover("")
2498                                .on_click(cx.listener({
2499                                    let contact = contact.clone();
2500                                    move |this, event: &ClickEvent, window, cx| {
2501                                        this.deploy_contact_context_menu(
2502                                            event.down.position,
2503                                            contact.clone(),
2504                                            window,
2505                                            cx,
2506                                        );
2507                                    }
2508                                })),
2509                        )
2510                    }),
2511            )
2512            .on_secondary_mouse_down(cx.listener({
2513                let contact = contact.clone();
2514                move |this, event: &MouseDownEvent, window, cx| {
2515                    this.deploy_contact_context_menu(event.position, contact.clone(), window, cx);
2516                }
2517            }))
2518            .start_slot(
2519                // todo handle contacts with no avatar
2520                Avatar::new(contact.user.avatar_uri.clone())
2521                    .indicator::<AvatarAvailabilityIndicator>(if online {
2522                        Some(AvatarAvailabilityIndicator::new(match busy {
2523                            true => ui::CollaboratorAvailability::Busy,
2524                            false => ui::CollaboratorAvailability::Free,
2525                        }))
2526                    } else {
2527                        None
2528                    }),
2529            );
2530
2531        div()
2532            .id(github_login.clone())
2533            .group("")
2534            .child(item)
2535            .tooltip(move |_, cx| {
2536                let text = if !online {
2537                    format!(" {} is offline", &github_login)
2538                } else if busy {
2539                    format!(" {} is on a call", &github_login)
2540                } else {
2541                    let room = ActiveCall::global(cx).read(cx).room();
2542                    if room.is_some() {
2543                        format!("Invite {} to join call", &github_login)
2544                    } else {
2545                        format!("Call {}", &github_login)
2546                    }
2547                };
2548                Tooltip::simple(text, cx)
2549            })
2550    }
2551
2552    fn render_contact_request(
2553        &self,
2554        user: &Arc<User>,
2555        is_incoming: bool,
2556        is_selected: bool,
2557        cx: &mut Context<Self>,
2558    ) -> impl IntoElement {
2559        let github_login = SharedString::from(user.github_login.clone());
2560        let user_id = user.id;
2561        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
2562        let color = if is_response_pending {
2563            Color::Muted
2564        } else {
2565            Color::Default
2566        };
2567
2568        let controls = if is_incoming {
2569            vec![
2570                IconButton::new("decline-contact", IconName::Close)
2571                    .on_click(cx.listener(move |this, _, window, cx| {
2572                        this.respond_to_contact_request(user_id, false, window, cx);
2573                    }))
2574                    .icon_color(color)
2575                    .tooltip(Tooltip::text("Decline invite")),
2576                IconButton::new("accept-contact", IconName::Check)
2577                    .on_click(cx.listener(move |this, _, window, cx| {
2578                        this.respond_to_contact_request(user_id, true, window, cx);
2579                    }))
2580                    .icon_color(color)
2581                    .tooltip(Tooltip::text("Accept invite")),
2582            ]
2583        } else {
2584            let github_login = github_login.clone();
2585            vec![
2586                IconButton::new("remove_contact", IconName::Close)
2587                    .on_click(cx.listener(move |this, _, window, cx| {
2588                        this.remove_contact(user_id, &github_login, window, cx);
2589                    }))
2590                    .icon_color(color)
2591                    .tooltip(Tooltip::text("Cancel invite")),
2592            ]
2593        };
2594
2595        ListItem::new(github_login.clone())
2596            .indent_level(1)
2597            .indent_step_size(px(20.))
2598            .toggle_state(is_selected)
2599            .child(
2600                h_flex()
2601                    .w_full()
2602                    .justify_between()
2603                    .child(Label::new(github_login.clone()))
2604                    .child(h_flex().children(controls)),
2605            )
2606            .start_slot(Avatar::new(user.avatar_uri.clone()))
2607    }
2608
2609    fn render_channel_invite(
2610        &self,
2611        channel: &Arc<Channel>,
2612        is_selected: bool,
2613        cx: &mut Context<Self>,
2614    ) -> ListItem {
2615        let channel_id = channel.id;
2616        let response_is_pending = self
2617            .channel_store
2618            .read(cx)
2619            .has_pending_channel_invite_response(channel);
2620        let color = if response_is_pending {
2621            Color::Muted
2622        } else {
2623            Color::Default
2624        };
2625
2626        let controls = [
2627            IconButton::new("reject-invite", IconName::Close)
2628                .on_click(cx.listener(move |this, _, _, cx| {
2629                    this.respond_to_channel_invite(channel_id, false, cx);
2630                }))
2631                .icon_color(color)
2632                .tooltip(Tooltip::text("Decline invite")),
2633            IconButton::new("accept-invite", IconName::Check)
2634                .on_click(cx.listener(move |this, _, _, cx| {
2635                    this.respond_to_channel_invite(channel_id, true, cx);
2636                }))
2637                .icon_color(color)
2638                .tooltip(Tooltip::text("Accept invite")),
2639        ];
2640
2641        ListItem::new(("channel-invite", channel.id.0 as usize))
2642            .toggle_state(is_selected)
2643            .child(
2644                h_flex()
2645                    .w_full()
2646                    .justify_between()
2647                    .child(Label::new(channel.name.clone()))
2648                    .child(h_flex().children(controls)),
2649            )
2650            .start_slot(
2651                Icon::new(IconName::Hash)
2652                    .size(IconSize::Small)
2653                    .color(Color::Muted),
2654            )
2655    }
2656
2657    fn render_contact_placeholder(&self, is_selected: bool, cx: &mut Context<Self>) -> ListItem {
2658        ListItem::new("contact-placeholder")
2659            .child(Icon::new(IconName::Plus))
2660            .child(Label::new("Add a Contact"))
2661            .toggle_state(is_selected)
2662            .on_click(cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)))
2663    }
2664
2665    fn render_channel(
2666        &self,
2667        channel: &Channel,
2668        depth: usize,
2669        has_children: bool,
2670        is_selected: bool,
2671        ix: usize,
2672        cx: &mut Context<Self>,
2673    ) -> impl IntoElement {
2674        let channel_id = channel.id;
2675
2676        let is_active = maybe!({
2677            let call_channel = ActiveCall::global(cx)
2678                .read(cx)
2679                .room()?
2680                .read(cx)
2681                .channel_id()?;
2682            Some(call_channel == channel_id)
2683        })
2684        .unwrap_or(false);
2685        let channel_store = self.channel_store.read(cx);
2686        let is_public = channel_store
2687            .channel_for_id(channel_id)
2688            .map(|channel| channel.visibility)
2689            == Some(proto::ChannelVisibility::Public);
2690        let disclosed =
2691            has_children.then(|| self.collapsed_channels.binary_search(&channel.id).is_err());
2692
2693        let has_messages_notification = channel_store.has_new_messages(channel_id);
2694        let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
2695
2696        const FACEPILE_LIMIT: usize = 3;
2697        let participants = self.channel_store.read(cx).channel_participants(channel_id);
2698
2699        let face_pile = if participants.is_empty() {
2700            None
2701        } else {
2702            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2703            let result = Facepile::new(
2704                participants
2705                    .iter()
2706                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
2707                    .take(FACEPILE_LIMIT)
2708                    .chain(if extra_count > 0 {
2709                        Some(
2710                            Label::new(format!("+{extra_count}"))
2711                                .ml_2()
2712                                .into_any_element(),
2713                        )
2714                    } else {
2715                        None
2716                    })
2717                    .collect::<SmallVec<_>>(),
2718            );
2719
2720            Some(result)
2721        };
2722
2723        let width = self.width.unwrap_or(px(240.));
2724        let root_id = channel.root_id();
2725
2726        div()
2727            .h_6()
2728            .id(channel_id.0 as usize)
2729            .group("")
2730            .flex()
2731            .w_full()
2732            .when(!channel.is_root_channel(), |el| {
2733                el.on_drag(channel.clone(), move |channel, _, _, cx| {
2734                    cx.new(|_| DraggedChannelView {
2735                        channel: channel.clone(),
2736                        width,
2737                    })
2738                })
2739            })
2740            .drag_over::<Channel>({
2741                move |style, dragged_channel: &Channel, _window, cx| {
2742                    if dragged_channel.root_id() == root_id {
2743                        style.bg(cx.theme().colors().ghost_element_hover)
2744                    } else {
2745                        style
2746                    }
2747                }
2748            })
2749            .on_drop(
2750                cx.listener(move |this, dragged_channel: &Channel, window, cx| {
2751                    if dragged_channel.root_id() != root_id {
2752                        return;
2753                    }
2754                    this.move_channel(dragged_channel.id, channel_id, window, cx);
2755                }),
2756            )
2757            .child(
2758                ListItem::new(channel_id.0 as usize)
2759                    // Add one level of depth for the disclosure arrow.
2760                    .indent_level(depth + 1)
2761                    .indent_step_size(px(20.))
2762                    .toggle_state(is_selected || is_active)
2763                    .toggle(disclosed)
2764                    .on_toggle(cx.listener(move |this, _, window, cx| {
2765                        this.toggle_channel_collapsed(channel_id, window, cx)
2766                    }))
2767                    .on_click(cx.listener(move |this, _, window, cx| {
2768                        if is_active {
2769                            this.open_channel_notes(channel_id, window, cx)
2770                        } else {
2771                            this.join_channel(channel_id, window, cx)
2772                        }
2773                    }))
2774                    .on_secondary_mouse_down(cx.listener(
2775                        move |this, event: &MouseDownEvent, window, cx| {
2776                            this.deploy_channel_context_menu(
2777                                event.position,
2778                                channel_id,
2779                                ix,
2780                                window,
2781                                cx,
2782                            )
2783                        },
2784                    ))
2785                    .start_slot(
2786                        div()
2787                            .relative()
2788                            .child(
2789                                Icon::new(if is_public {
2790                                    IconName::Public
2791                                } else {
2792                                    IconName::Hash
2793                                })
2794                                .size(IconSize::Small)
2795                                .color(Color::Muted),
2796                            )
2797                            .children(has_notes_notification.then(|| {
2798                                div()
2799                                    .w_1p5()
2800                                    .absolute()
2801                                    .right(px(-1.))
2802                                    .top(px(-1.))
2803                                    .child(Indicator::dot().color(Color::Info))
2804                            })),
2805                    )
2806                    .child(
2807                        h_flex()
2808                            .id(channel_id.0 as usize)
2809                            .child(Label::new(channel.name.clone()))
2810                            .children(face_pile.map(|face_pile| face_pile.p_1())),
2811                    ),
2812            )
2813            .child(
2814                h_flex().absolute().right(rems(0.)).h_full().child(
2815                    h_flex()
2816                        .h_full()
2817                        .gap_1()
2818                        .px_1()
2819                        .child(
2820                            IconButton::new("channel_chat", IconName::MessageBubbles)
2821                                .style(ButtonStyle::Filled)
2822                                .shape(ui::IconButtonShape::Square)
2823                                .icon_size(IconSize::Small)
2824                                .icon_color(if has_messages_notification {
2825                                    Color::Default
2826                                } else {
2827                                    Color::Muted
2828                                })
2829                                .on_click(cx.listener(move |this, _, window, cx| {
2830                                    this.join_channel_chat(channel_id, window, cx)
2831                                }))
2832                                .tooltip(Tooltip::text("Open channel chat"))
2833                                .visible_on_hover(""),
2834                        )
2835                        .child(
2836                            IconButton::new("channel_notes", IconName::File)
2837                                .style(ButtonStyle::Filled)
2838                                .shape(ui::IconButtonShape::Square)
2839                                .icon_size(IconSize::Small)
2840                                .icon_color(if has_notes_notification {
2841                                    Color::Default
2842                                } else {
2843                                    Color::Muted
2844                                })
2845                                .on_click(cx.listener(move |this, _, window, cx| {
2846                                    this.open_channel_notes(channel_id, window, cx)
2847                                }))
2848                                .tooltip(Tooltip::text("Open channel notes"))
2849                                .visible_on_hover(""),
2850                        ),
2851                ),
2852            )
2853            .tooltip({
2854                let channel_store = self.channel_store.clone();
2855                move |_window, cx| {
2856                    cx.new(|_| JoinChannelTooltip {
2857                        channel_store: channel_store.clone(),
2858                        channel_id,
2859                        has_notes_notification,
2860                    })
2861                    .into()
2862                }
2863            })
2864    }
2865
2866    fn render_channel_editor(
2867        &self,
2868        depth: usize,
2869        _window: &mut Window,
2870        _cx: &mut Context<Self>,
2871    ) -> impl IntoElement {
2872        let item = ListItem::new("channel-editor")
2873            .inset(false)
2874            // Add one level of depth for the disclosure arrow.
2875            .indent_level(depth + 1)
2876            .indent_step_size(px(20.))
2877            .start_slot(
2878                Icon::new(IconName::Hash)
2879                    .size(IconSize::Small)
2880                    .color(Color::Muted),
2881            );
2882
2883        if let Some(pending_name) = self
2884            .channel_editing_state
2885            .as_ref()
2886            .and_then(|state| state.pending_name())
2887        {
2888            item.child(Label::new(pending_name))
2889        } else {
2890            item.child(self.channel_name_editor.clone())
2891        }
2892    }
2893}
2894
2895fn render_tree_branch(
2896    is_last: bool,
2897    overdraw: bool,
2898    window: &mut Window,
2899    cx: &mut App,
2900) -> impl IntoElement {
2901    let rem_size = window.rem_size();
2902    let line_height = window.text_style().line_height_in_pixels(rem_size);
2903    let width = rem_size * 1.5;
2904    let thickness = px(1.);
2905    let color = cx.theme().colors().text;
2906
2907    canvas(
2908        |_, _, _| {},
2909        move |bounds, _, window, _| {
2910            let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2911            let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2912            let right = bounds.right();
2913            let top = bounds.top();
2914
2915            window.paint_quad(fill(
2916                Bounds::from_corners(
2917                    point(start_x, top),
2918                    point(
2919                        start_x + thickness,
2920                        if is_last {
2921                            start_y
2922                        } else {
2923                            bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
2924                        },
2925                    ),
2926                ),
2927                color,
2928            ));
2929            window.paint_quad(fill(
2930                Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
2931                color,
2932            ));
2933        },
2934    )
2935    .w(width)
2936    .h(line_height)
2937}
2938
2939impl Render for CollabPanel {
2940    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2941        v_flex()
2942            .key_context("CollabPanel")
2943            .on_action(cx.listener(CollabPanel::cancel))
2944            .on_action(cx.listener(CollabPanel::select_next))
2945            .on_action(cx.listener(CollabPanel::select_previous))
2946            .on_action(cx.listener(CollabPanel::confirm))
2947            .on_action(cx.listener(CollabPanel::insert_space))
2948            .on_action(cx.listener(CollabPanel::remove_selected_channel))
2949            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
2950            .on_action(cx.listener(CollabPanel::rename_selected_channel))
2951            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
2952            .on_action(cx.listener(CollabPanel::expand_selected_channel))
2953            .on_action(cx.listener(CollabPanel::start_move_selected_channel))
2954            .track_focus(&self.focus_handle(cx))
2955            .size_full()
2956            .child(if self.user_store.read(cx).current_user().is_none() {
2957                self.render_signed_out(cx)
2958            } else {
2959                self.render_signed_in(window, cx)
2960            })
2961            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2962                deferred(
2963                    anchored()
2964                        .position(*position)
2965                        .anchor(gpui::Corner::TopLeft)
2966                        .child(menu.clone()),
2967                )
2968                .with_priority(1)
2969            }))
2970    }
2971}
2972
2973impl EventEmitter<PanelEvent> for CollabPanel {}
2974
2975impl Panel for CollabPanel {
2976    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
2977        CollaborationPanelSettings::get_global(cx).dock
2978    }
2979
2980    fn position_is_valid(&self, position: DockPosition) -> bool {
2981        matches!(position, DockPosition::Left | DockPosition::Right)
2982    }
2983
2984    fn set_position(
2985        &mut self,
2986        position: DockPosition,
2987        _window: &mut Window,
2988        cx: &mut Context<Self>,
2989    ) {
2990        settings::update_settings_file::<CollaborationPanelSettings>(
2991            self.fs.clone(),
2992            cx,
2993            move |settings, _| settings.dock = Some(position),
2994        );
2995    }
2996
2997    fn size(&self, _window: &Window, cx: &App) -> Pixels {
2998        self.width
2999            .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
3000    }
3001
3002    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
3003        self.width = size;
3004        self.serialize(cx);
3005        cx.notify();
3006    }
3007
3008    fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
3009        CollaborationPanelSettings::get_global(cx)
3010            .button
3011            .then_some(ui::IconName::UserGroup)
3012    }
3013
3014    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
3015        Some("Collab Panel")
3016    }
3017
3018    fn toggle_action(&self) -> Box<dyn gpui::Action> {
3019        Box::new(ToggleFocus)
3020    }
3021
3022    fn persistent_name() -> &'static str {
3023        "CollabPanel"
3024    }
3025
3026    fn activation_priority(&self) -> u32 {
3027        6
3028    }
3029}
3030
3031impl Focusable for CollabPanel {
3032    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
3033        self.filter_editor.focus_handle(cx).clone()
3034    }
3035}
3036
3037impl PartialEq for ListEntry {
3038    fn eq(&self, other: &Self) -> bool {
3039        match self {
3040            ListEntry::Header(section_1) => {
3041                if let ListEntry::Header(section_2) = other {
3042                    return section_1 == section_2;
3043                }
3044            }
3045            ListEntry::CallParticipant { user: user_1, .. } => {
3046                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3047                    return user_1.id == user_2.id;
3048                }
3049            }
3050            ListEntry::ParticipantProject {
3051                project_id: project_id_1,
3052                ..
3053            } => {
3054                if let ListEntry::ParticipantProject {
3055                    project_id: project_id_2,
3056                    ..
3057                } = other
3058                {
3059                    return project_id_1 == project_id_2;
3060                }
3061            }
3062            ListEntry::ParticipantScreen {
3063                peer_id: peer_id_1, ..
3064            } => {
3065                if let ListEntry::ParticipantScreen {
3066                    peer_id: peer_id_2, ..
3067                } = other
3068                {
3069                    return peer_id_1 == peer_id_2;
3070                }
3071            }
3072            ListEntry::Channel {
3073                channel: channel_1, ..
3074            } => {
3075                if let ListEntry::Channel {
3076                    channel: channel_2, ..
3077                } = other
3078                {
3079                    return channel_1.id == channel_2.id;
3080                }
3081            }
3082            ListEntry::ChannelNotes { channel_id } => {
3083                if let ListEntry::ChannelNotes {
3084                    channel_id: other_id,
3085                } = other
3086                {
3087                    return channel_id == other_id;
3088                }
3089            }
3090            ListEntry::ChannelChat { channel_id } => {
3091                if let ListEntry::ChannelChat {
3092                    channel_id: other_id,
3093                } = other
3094                {
3095                    return channel_id == other_id;
3096                }
3097            }
3098            ListEntry::ChannelInvite(channel_1) => {
3099                if let ListEntry::ChannelInvite(channel_2) = other {
3100                    return channel_1.id == channel_2.id;
3101                }
3102            }
3103            ListEntry::IncomingRequest(user_1) => {
3104                if let ListEntry::IncomingRequest(user_2) = other {
3105                    return user_1.id == user_2.id;
3106                }
3107            }
3108            ListEntry::OutgoingRequest(user_1) => {
3109                if let ListEntry::OutgoingRequest(user_2) = other {
3110                    return user_1.id == user_2.id;
3111                }
3112            }
3113            ListEntry::Contact {
3114                contact: contact_1, ..
3115            } => {
3116                if let ListEntry::Contact {
3117                    contact: contact_2, ..
3118                } = other
3119                {
3120                    return contact_1.user.id == contact_2.user.id;
3121                }
3122            }
3123            ListEntry::ChannelEditor { depth } => {
3124                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3125                    return depth == other_depth;
3126                }
3127            }
3128            ListEntry::ContactPlaceholder => {
3129                if let ListEntry::ContactPlaceholder = other {
3130                    return true;
3131                }
3132            }
3133        }
3134        false
3135    }
3136}
3137
3138struct DraggedChannelView {
3139    channel: Channel,
3140    width: Pixels,
3141}
3142
3143impl Render for DraggedChannelView {
3144    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3145        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3146        h_flex()
3147            .font_family(ui_font)
3148            .bg(cx.theme().colors().background)
3149            .w(self.width)
3150            .p_1()
3151            .gap_1()
3152            .child(
3153                Icon::new(
3154                    if self.channel.visibility == proto::ChannelVisibility::Public {
3155                        IconName::Public
3156                    } else {
3157                        IconName::Hash
3158                    },
3159                )
3160                .size(IconSize::Small)
3161                .color(Color::Muted),
3162            )
3163            .child(Label::new(self.channel.name.clone()))
3164    }
3165}
3166
3167struct JoinChannelTooltip {
3168    channel_store: Entity<ChannelStore>,
3169    channel_id: ChannelId,
3170    #[allow(unused)]
3171    has_notes_notification: bool,
3172}
3173
3174impl Render for JoinChannelTooltip {
3175    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3176        tooltip_container(window, cx, |container, _, cx| {
3177            let participants = self
3178                .channel_store
3179                .read(cx)
3180                .channel_participants(self.channel_id);
3181
3182            container
3183                .child(Label::new("Join channel"))
3184                .children(participants.iter().map(|participant| {
3185                    h_flex()
3186                        .gap_2()
3187                        .child(Avatar::new(participant.avatar_uri.clone()))
3188                        .child(Label::new(participant.github_login.clone()))
3189                }))
3190        })
3191    }
3192}