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                                        .notify_async_err(cx);
2231                                })
2232                                .detach()
2233                            })),
2234                    )
2235                    .child(
2236                        div().flex().w_full().items_center().child(
2237                            Label::new("Sign in to enable collaboration.")
2238                                .color(Color::Muted)
2239                                .size(LabelSize::Small),
2240                        ),
2241                    ),
2242            )
2243    }
2244
2245    fn render_list_entry(
2246        &mut self,
2247        ix: usize,
2248        window: &mut Window,
2249        cx: &mut Context<Self>,
2250    ) -> AnyElement {
2251        let entry = &self.entries[ix];
2252
2253        let is_selected = self.selection == Some(ix);
2254        match entry {
2255            ListEntry::Header(section) => {
2256                let is_collapsed = self.collapsed_sections.contains(section);
2257                self.render_header(*section, is_selected, is_collapsed, cx)
2258                    .into_any_element()
2259            }
2260            ListEntry::Contact { contact, calling } => self
2261                .render_contact(contact, *calling, is_selected, cx)
2262                .into_any_element(),
2263            ListEntry::ContactPlaceholder => self
2264                .render_contact_placeholder(is_selected, cx)
2265                .into_any_element(),
2266            ListEntry::IncomingRequest(user) => self
2267                .render_contact_request(user, true, is_selected, cx)
2268                .into_any_element(),
2269            ListEntry::OutgoingRequest(user) => self
2270                .render_contact_request(user, false, is_selected, cx)
2271                .into_any_element(),
2272            ListEntry::Channel {
2273                channel,
2274                depth,
2275                has_children,
2276            } => self
2277                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
2278                .into_any_element(),
2279            ListEntry::ChannelEditor { depth } => self
2280                .render_channel_editor(*depth, window, cx)
2281                .into_any_element(),
2282            ListEntry::ChannelInvite(channel) => self
2283                .render_channel_invite(channel, is_selected, cx)
2284                .into_any_element(),
2285            ListEntry::CallParticipant {
2286                user,
2287                peer_id,
2288                is_pending,
2289                role,
2290            } => self
2291                .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
2292                .into_any_element(),
2293            ListEntry::ParticipantProject {
2294                project_id,
2295                worktree_root_names,
2296                host_user_id,
2297                is_last,
2298            } => self
2299                .render_participant_project(
2300                    *project_id,
2301                    worktree_root_names,
2302                    *host_user_id,
2303                    *is_last,
2304                    is_selected,
2305                    window,
2306                    cx,
2307                )
2308                .into_any_element(),
2309            ListEntry::ParticipantScreen { peer_id, is_last } => self
2310                .render_participant_screen(*peer_id, *is_last, is_selected, window, cx)
2311                .into_any_element(),
2312            ListEntry::ChannelNotes { channel_id } => self
2313                .render_channel_notes(*channel_id, is_selected, window, cx)
2314                .into_any_element(),
2315            ListEntry::ChannelChat { channel_id } => self
2316                .render_channel_chat(*channel_id, is_selected, window, cx)
2317                .into_any_element(),
2318        }
2319    }
2320
2321    fn render_signed_in(&mut self, _: &mut Window, cx: &mut Context<Self>) -> Div {
2322        self.channel_store.update(cx, |channel_store, _| {
2323            channel_store.initialize();
2324        });
2325        v_flex()
2326            .size_full()
2327            .child(list(self.list_state.clone()).size_full())
2328            .child(
2329                v_flex()
2330                    .child(div().mx_2().border_primary(cx).border_t_1())
2331                    .child(
2332                        v_flex()
2333                            .p_2()
2334                            .child(self.render_filter_input(&self.filter_editor, cx)),
2335                    ),
2336            )
2337    }
2338
2339    fn render_filter_input(
2340        &self,
2341        editor: &Entity<Editor>,
2342        cx: &mut Context<Self>,
2343    ) -> impl IntoElement {
2344        let settings = ThemeSettings::get_global(cx);
2345        let text_style = TextStyle {
2346            color: if editor.read(cx).read_only(cx) {
2347                cx.theme().colors().text_disabled
2348            } else {
2349                cx.theme().colors().text
2350            },
2351            font_family: settings.ui_font.family.clone(),
2352            font_features: settings.ui_font.features.clone(),
2353            font_fallbacks: settings.ui_font.fallbacks.clone(),
2354            font_size: rems(0.875).into(),
2355            font_weight: settings.ui_font.weight,
2356            font_style: FontStyle::Normal,
2357            line_height: relative(1.3),
2358            ..Default::default()
2359        };
2360
2361        EditorElement::new(
2362            editor,
2363            EditorStyle {
2364                local_player: cx.theme().players().local(),
2365                text: text_style,
2366                ..Default::default()
2367            },
2368        )
2369    }
2370
2371    fn render_header(
2372        &self,
2373        section: Section,
2374        is_selected: bool,
2375        is_collapsed: bool,
2376        cx: &mut Context<Self>,
2377    ) -> impl IntoElement {
2378        let mut channel_link = None;
2379        let mut channel_tooltip_text = None;
2380        let mut channel_icon = None;
2381
2382        let text = match section {
2383            Section::ActiveCall => {
2384                let channel_name = maybe!({
2385                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2386
2387                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2388
2389                    channel_link = Some(channel.link(cx));
2390                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2391                        proto::ChannelVisibility::Public => {
2392                            (Some("icons/public.svg"), Some("Copy public channel link."))
2393                        }
2394                        proto::ChannelVisibility::Members => {
2395                            (Some("icons/hash.svg"), Some("Copy private channel link."))
2396                        }
2397                    };
2398
2399                    Some(channel.name.as_ref())
2400                });
2401
2402                if let Some(name) = channel_name {
2403                    SharedString::from(name.to_string())
2404                } else {
2405                    SharedString::from("Current Call")
2406                }
2407            }
2408            Section::ContactRequests => SharedString::from("Requests"),
2409            Section::Contacts => SharedString::from("Contacts"),
2410            Section::Channels => SharedString::from("Channels"),
2411            Section::ChannelInvites => SharedString::from("Invites"),
2412            Section::Online => SharedString::from("Online"),
2413            Section::Offline => SharedString::from("Offline"),
2414        };
2415
2416        let button = match section {
2417            Section::ActiveCall => channel_link.map(|channel_link| {
2418                let channel_link_copy = channel_link.clone();
2419                IconButton::new("channel-link", IconName::Copy)
2420                    .icon_size(IconSize::Small)
2421                    .size(ButtonSize::None)
2422                    .visible_on_hover("section-header")
2423                    .on_click(move |_, _, cx| {
2424                        let item = ClipboardItem::new_string(channel_link_copy.clone());
2425                        cx.write_to_clipboard(item)
2426                    })
2427                    .tooltip(Tooltip::text("Copy channel link"))
2428                    .into_any_element()
2429            }),
2430            Section::Contacts => Some(
2431                IconButton::new("add-contact", IconName::Plus)
2432                    .on_click(
2433                        cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)),
2434                    )
2435                    .tooltip(Tooltip::text("Search for new contact"))
2436                    .into_any_element(),
2437            ),
2438            Section::Channels => Some(
2439                IconButton::new("add-channel", IconName::Plus)
2440                    .on_click(cx.listener(|this, _, window, cx| this.new_root_channel(window, cx)))
2441                    .tooltip(Tooltip::text("Create a channel"))
2442                    .into_any_element(),
2443            ),
2444            _ => None,
2445        };
2446
2447        let can_collapse = match section {
2448            Section::ActiveCall | Section::Channels | Section::Contacts => false,
2449            Section::ChannelInvites
2450            | Section::ContactRequests
2451            | Section::Online
2452            | Section::Offline => true,
2453        };
2454
2455        h_flex().w_full().group("section-header").child(
2456            ListHeader::new(text)
2457                .when(can_collapse, |header| {
2458                    header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
2459                        move |this, _, _, cx| {
2460                            this.toggle_section_expanded(section, cx);
2461                        },
2462                    ))
2463                })
2464                .inset(true)
2465                .end_slot::<AnyElement>(button)
2466                .toggle_state(is_selected),
2467        )
2468    }
2469
2470    fn render_contact(
2471        &self,
2472        contact: &Arc<Contact>,
2473        calling: bool,
2474        is_selected: bool,
2475        cx: &mut Context<Self>,
2476    ) -> impl IntoElement {
2477        let online = contact.online;
2478        let busy = contact.busy || calling;
2479        let github_login = SharedString::from(contact.user.github_login.clone());
2480        let item = ListItem::new(github_login.clone())
2481            .indent_level(1)
2482            .indent_step_size(px(20.))
2483            .toggle_state(is_selected)
2484            .child(
2485                h_flex()
2486                    .w_full()
2487                    .justify_between()
2488                    .child(Label::new(github_login.clone()))
2489                    .when(calling, |el| {
2490                        el.child(Label::new("Calling").color(Color::Muted))
2491                    })
2492                    .when(!calling, |el| {
2493                        el.child(
2494                            IconButton::new("contact context menu", IconName::Ellipsis)
2495                                .icon_color(Color::Muted)
2496                                .visible_on_hover("")
2497                                .on_click(cx.listener({
2498                                    let contact = contact.clone();
2499                                    move |this, event: &ClickEvent, window, cx| {
2500                                        this.deploy_contact_context_menu(
2501                                            event.down.position,
2502                                            contact.clone(),
2503                                            window,
2504                                            cx,
2505                                        );
2506                                    }
2507                                })),
2508                        )
2509                    }),
2510            )
2511            .on_secondary_mouse_down(cx.listener({
2512                let contact = contact.clone();
2513                move |this, event: &MouseDownEvent, window, cx| {
2514                    this.deploy_contact_context_menu(event.position, contact.clone(), window, cx);
2515                }
2516            }))
2517            .start_slot(
2518                // todo handle contacts with no avatar
2519                Avatar::new(contact.user.avatar_uri.clone())
2520                    .indicator::<AvatarAvailabilityIndicator>(if online {
2521                        Some(AvatarAvailabilityIndicator::new(match busy {
2522                            true => ui::CollaboratorAvailability::Busy,
2523                            false => ui::CollaboratorAvailability::Free,
2524                        }))
2525                    } else {
2526                        None
2527                    }),
2528            );
2529
2530        div()
2531            .id(github_login.clone())
2532            .group("")
2533            .child(item)
2534            .tooltip(move |_, cx| {
2535                let text = if !online {
2536                    format!(" {} is offline", &github_login)
2537                } else if busy {
2538                    format!(" {} is on a call", &github_login)
2539                } else {
2540                    let room = ActiveCall::global(cx).read(cx).room();
2541                    if room.is_some() {
2542                        format!("Invite {} to join call", &github_login)
2543                    } else {
2544                        format!("Call {}", &github_login)
2545                    }
2546                };
2547                Tooltip::simple(text, cx)
2548            })
2549    }
2550
2551    fn render_contact_request(
2552        &self,
2553        user: &Arc<User>,
2554        is_incoming: bool,
2555        is_selected: bool,
2556        cx: &mut Context<Self>,
2557    ) -> impl IntoElement {
2558        let github_login = SharedString::from(user.github_login.clone());
2559        let user_id = user.id;
2560        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
2561        let color = if is_response_pending {
2562            Color::Muted
2563        } else {
2564            Color::Default
2565        };
2566
2567        let controls = if is_incoming {
2568            vec![
2569                IconButton::new("decline-contact", IconName::Close)
2570                    .on_click(cx.listener(move |this, _, window, cx| {
2571                        this.respond_to_contact_request(user_id, false, window, cx);
2572                    }))
2573                    .icon_color(color)
2574                    .tooltip(Tooltip::text("Decline invite")),
2575                IconButton::new("accept-contact", IconName::Check)
2576                    .on_click(cx.listener(move |this, _, window, cx| {
2577                        this.respond_to_contact_request(user_id, true, window, cx);
2578                    }))
2579                    .icon_color(color)
2580                    .tooltip(Tooltip::text("Accept invite")),
2581            ]
2582        } else {
2583            let github_login = github_login.clone();
2584            vec![
2585                IconButton::new("remove_contact", IconName::Close)
2586                    .on_click(cx.listener(move |this, _, window, cx| {
2587                        this.remove_contact(user_id, &github_login, window, cx);
2588                    }))
2589                    .icon_color(color)
2590                    .tooltip(Tooltip::text("Cancel invite")),
2591            ]
2592        };
2593
2594        ListItem::new(github_login.clone())
2595            .indent_level(1)
2596            .indent_step_size(px(20.))
2597            .toggle_state(is_selected)
2598            .child(
2599                h_flex()
2600                    .w_full()
2601                    .justify_between()
2602                    .child(Label::new(github_login.clone()))
2603                    .child(h_flex().children(controls)),
2604            )
2605            .start_slot(Avatar::new(user.avatar_uri.clone()))
2606    }
2607
2608    fn render_channel_invite(
2609        &self,
2610        channel: &Arc<Channel>,
2611        is_selected: bool,
2612        cx: &mut Context<Self>,
2613    ) -> ListItem {
2614        let channel_id = channel.id;
2615        let response_is_pending = self
2616            .channel_store
2617            .read(cx)
2618            .has_pending_channel_invite_response(channel);
2619        let color = if response_is_pending {
2620            Color::Muted
2621        } else {
2622            Color::Default
2623        };
2624
2625        let controls = [
2626            IconButton::new("reject-invite", IconName::Close)
2627                .on_click(cx.listener(move |this, _, _, cx| {
2628                    this.respond_to_channel_invite(channel_id, false, cx);
2629                }))
2630                .icon_color(color)
2631                .tooltip(Tooltip::text("Decline invite")),
2632            IconButton::new("accept-invite", IconName::Check)
2633                .on_click(cx.listener(move |this, _, _, cx| {
2634                    this.respond_to_channel_invite(channel_id, true, cx);
2635                }))
2636                .icon_color(color)
2637                .tooltip(Tooltip::text("Accept invite")),
2638        ];
2639
2640        ListItem::new(("channel-invite", channel.id.0 as usize))
2641            .toggle_state(is_selected)
2642            .child(
2643                h_flex()
2644                    .w_full()
2645                    .justify_between()
2646                    .child(Label::new(channel.name.clone()))
2647                    .child(h_flex().children(controls)),
2648            )
2649            .start_slot(
2650                Icon::new(IconName::Hash)
2651                    .size(IconSize::Small)
2652                    .color(Color::Muted),
2653            )
2654    }
2655
2656    fn render_contact_placeholder(&self, is_selected: bool, cx: &mut Context<Self>) -> ListItem {
2657        ListItem::new("contact-placeholder")
2658            .child(Icon::new(IconName::Plus))
2659            .child(Label::new("Add a Contact"))
2660            .toggle_state(is_selected)
2661            .on_click(cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)))
2662    }
2663
2664    fn render_channel(
2665        &self,
2666        channel: &Channel,
2667        depth: usize,
2668        has_children: bool,
2669        is_selected: bool,
2670        ix: usize,
2671        cx: &mut Context<Self>,
2672    ) -> impl IntoElement {
2673        let channel_id = channel.id;
2674
2675        let is_active = maybe!({
2676            let call_channel = ActiveCall::global(cx)
2677                .read(cx)
2678                .room()?
2679                .read(cx)
2680                .channel_id()?;
2681            Some(call_channel == channel_id)
2682        })
2683        .unwrap_or(false);
2684        let channel_store = self.channel_store.read(cx);
2685        let is_public = channel_store
2686            .channel_for_id(channel_id)
2687            .map(|channel| channel.visibility)
2688            == Some(proto::ChannelVisibility::Public);
2689        let disclosed =
2690            has_children.then(|| self.collapsed_channels.binary_search(&channel.id).is_err());
2691
2692        let has_messages_notification = channel_store.has_new_messages(channel_id);
2693        let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
2694
2695        const FACEPILE_LIMIT: usize = 3;
2696        let participants = self.channel_store.read(cx).channel_participants(channel_id);
2697
2698        let face_pile = if participants.is_empty() {
2699            None
2700        } else {
2701            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2702            let result = Facepile::new(
2703                participants
2704                    .iter()
2705                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
2706                    .take(FACEPILE_LIMIT)
2707                    .chain(if extra_count > 0 {
2708                        Some(
2709                            Label::new(format!("+{extra_count}"))
2710                                .ml_2()
2711                                .into_any_element(),
2712                        )
2713                    } else {
2714                        None
2715                    })
2716                    .collect::<SmallVec<_>>(),
2717            );
2718
2719            Some(result)
2720        };
2721
2722        let width = self.width.unwrap_or(px(240.));
2723        let root_id = channel.root_id();
2724
2725        div()
2726            .h_6()
2727            .id(channel_id.0 as usize)
2728            .group("")
2729            .flex()
2730            .w_full()
2731            .when(!channel.is_root_channel(), |el| {
2732                el.on_drag(channel.clone(), move |channel, _, _, cx| {
2733                    cx.new(|_| DraggedChannelView {
2734                        channel: channel.clone(),
2735                        width,
2736                    })
2737                })
2738            })
2739            .drag_over::<Channel>({
2740                move |style, dragged_channel: &Channel, _window, cx| {
2741                    if dragged_channel.root_id() == root_id {
2742                        style.bg(cx.theme().colors().ghost_element_hover)
2743                    } else {
2744                        style
2745                    }
2746                }
2747            })
2748            .on_drop(
2749                cx.listener(move |this, dragged_channel: &Channel, window, cx| {
2750                    if dragged_channel.root_id() != root_id {
2751                        return;
2752                    }
2753                    this.move_channel(dragged_channel.id, channel_id, window, cx);
2754                }),
2755            )
2756            .child(
2757                ListItem::new(channel_id.0 as usize)
2758                    // Add one level of depth for the disclosure arrow.
2759                    .indent_level(depth + 1)
2760                    .indent_step_size(px(20.))
2761                    .toggle_state(is_selected || is_active)
2762                    .toggle(disclosed)
2763                    .on_toggle(cx.listener(move |this, _, window, cx| {
2764                        this.toggle_channel_collapsed(channel_id, window, cx)
2765                    }))
2766                    .on_click(cx.listener(move |this, _, window, cx| {
2767                        if is_active {
2768                            this.open_channel_notes(channel_id, window, cx)
2769                        } else {
2770                            this.join_channel(channel_id, window, cx)
2771                        }
2772                    }))
2773                    .on_secondary_mouse_down(cx.listener(
2774                        move |this, event: &MouseDownEvent, window, cx| {
2775                            this.deploy_channel_context_menu(
2776                                event.position,
2777                                channel_id,
2778                                ix,
2779                                window,
2780                                cx,
2781                            )
2782                        },
2783                    ))
2784                    .start_slot(
2785                        div()
2786                            .relative()
2787                            .child(
2788                                Icon::new(if is_public {
2789                                    IconName::Public
2790                                } else {
2791                                    IconName::Hash
2792                                })
2793                                .size(IconSize::Small)
2794                                .color(Color::Muted),
2795                            )
2796                            .children(has_notes_notification.then(|| {
2797                                div()
2798                                    .w_1p5()
2799                                    .absolute()
2800                                    .right(px(-1.))
2801                                    .top(px(-1.))
2802                                    .child(Indicator::dot().color(Color::Info))
2803                            })),
2804                    )
2805                    .child(
2806                        h_flex()
2807                            .id(channel_id.0 as usize)
2808                            .child(Label::new(channel.name.clone()))
2809                            .children(face_pile.map(|face_pile| face_pile.p_1())),
2810                    ),
2811            )
2812            .child(
2813                h_flex().absolute().right(rems(0.)).h_full().child(
2814                    h_flex()
2815                        .h_full()
2816                        .gap_1()
2817                        .px_1()
2818                        .child(
2819                            IconButton::new("channel_chat", IconName::MessageBubbles)
2820                                .style(ButtonStyle::Filled)
2821                                .shape(ui::IconButtonShape::Square)
2822                                .icon_size(IconSize::Small)
2823                                .icon_color(if has_messages_notification {
2824                                    Color::Default
2825                                } else {
2826                                    Color::Muted
2827                                })
2828                                .on_click(cx.listener(move |this, _, window, cx| {
2829                                    this.join_channel_chat(channel_id, window, cx)
2830                                }))
2831                                .tooltip(Tooltip::text("Open channel chat"))
2832                                .visible_on_hover(""),
2833                        )
2834                        .child(
2835                            IconButton::new("channel_notes", IconName::File)
2836                                .style(ButtonStyle::Filled)
2837                                .shape(ui::IconButtonShape::Square)
2838                                .icon_size(IconSize::Small)
2839                                .icon_color(if has_notes_notification {
2840                                    Color::Default
2841                                } else {
2842                                    Color::Muted
2843                                })
2844                                .on_click(cx.listener(move |this, _, window, cx| {
2845                                    this.open_channel_notes(channel_id, window, cx)
2846                                }))
2847                                .tooltip(Tooltip::text("Open channel notes"))
2848                                .visible_on_hover(""),
2849                        ),
2850                ),
2851            )
2852            .tooltip({
2853                let channel_store = self.channel_store.clone();
2854                move |_window, cx| {
2855                    cx.new(|_| JoinChannelTooltip {
2856                        channel_store: channel_store.clone(),
2857                        channel_id,
2858                        has_notes_notification,
2859                    })
2860                    .into()
2861                }
2862            })
2863    }
2864
2865    fn render_channel_editor(
2866        &self,
2867        depth: usize,
2868        _window: &mut Window,
2869        _cx: &mut Context<Self>,
2870    ) -> impl IntoElement {
2871        let item = ListItem::new("channel-editor")
2872            .inset(false)
2873            // Add one level of depth for the disclosure arrow.
2874            .indent_level(depth + 1)
2875            .indent_step_size(px(20.))
2876            .start_slot(
2877                Icon::new(IconName::Hash)
2878                    .size(IconSize::Small)
2879                    .color(Color::Muted),
2880            );
2881
2882        if let Some(pending_name) = self
2883            .channel_editing_state
2884            .as_ref()
2885            .and_then(|state| state.pending_name())
2886        {
2887            item.child(Label::new(pending_name))
2888        } else {
2889            item.child(self.channel_name_editor.clone())
2890        }
2891    }
2892}
2893
2894fn render_tree_branch(
2895    is_last: bool,
2896    overdraw: bool,
2897    window: &mut Window,
2898    cx: &mut App,
2899) -> impl IntoElement {
2900    let rem_size = window.rem_size();
2901    let line_height = window.text_style().line_height_in_pixels(rem_size);
2902    let width = rem_size * 1.5;
2903    let thickness = px(1.);
2904    let color = cx.theme().colors().text;
2905
2906    canvas(
2907        |_, _, _| {},
2908        move |bounds, _, window, _| {
2909            let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2910            let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2911            let right = bounds.right();
2912            let top = bounds.top();
2913
2914            window.paint_quad(fill(
2915                Bounds::from_corners(
2916                    point(start_x, top),
2917                    point(
2918                        start_x + thickness,
2919                        if is_last {
2920                            start_y
2921                        } else {
2922                            bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
2923                        },
2924                    ),
2925                ),
2926                color,
2927            ));
2928            window.paint_quad(fill(
2929                Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
2930                color,
2931            ));
2932        },
2933    )
2934    .w(width)
2935    .h(line_height)
2936}
2937
2938impl Render for CollabPanel {
2939    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2940        v_flex()
2941            .key_context("CollabPanel")
2942            .on_action(cx.listener(CollabPanel::cancel))
2943            .on_action(cx.listener(CollabPanel::select_next))
2944            .on_action(cx.listener(CollabPanel::select_previous))
2945            .on_action(cx.listener(CollabPanel::confirm))
2946            .on_action(cx.listener(CollabPanel::insert_space))
2947            .on_action(cx.listener(CollabPanel::remove_selected_channel))
2948            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
2949            .on_action(cx.listener(CollabPanel::rename_selected_channel))
2950            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
2951            .on_action(cx.listener(CollabPanel::expand_selected_channel))
2952            .on_action(cx.listener(CollabPanel::start_move_selected_channel))
2953            .track_focus(&self.focus_handle(cx))
2954            .size_full()
2955            .child(if self.user_store.read(cx).current_user().is_none() {
2956                self.render_signed_out(cx)
2957            } else {
2958                self.render_signed_in(window, cx)
2959            })
2960            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2961                deferred(
2962                    anchored()
2963                        .position(*position)
2964                        .anchor(gpui::Corner::TopLeft)
2965                        .child(menu.clone()),
2966                )
2967                .with_priority(1)
2968            }))
2969    }
2970}
2971
2972impl EventEmitter<PanelEvent> for CollabPanel {}
2973
2974impl Panel for CollabPanel {
2975    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
2976        CollaborationPanelSettings::get_global(cx).dock
2977    }
2978
2979    fn position_is_valid(&self, position: DockPosition) -> bool {
2980        matches!(position, DockPosition::Left | DockPosition::Right)
2981    }
2982
2983    fn set_position(
2984        &mut self,
2985        position: DockPosition,
2986        _window: &mut Window,
2987        cx: &mut Context<Self>,
2988    ) {
2989        settings::update_settings_file::<CollaborationPanelSettings>(
2990            self.fs.clone(),
2991            cx,
2992            move |settings, _| settings.dock = Some(position),
2993        );
2994    }
2995
2996    fn size(&self, _window: &Window, cx: &App) -> Pixels {
2997        self.width
2998            .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
2999    }
3000
3001    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
3002        self.width = size;
3003        self.serialize(cx);
3004        cx.notify();
3005    }
3006
3007    fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
3008        CollaborationPanelSettings::get_global(cx)
3009            .button
3010            .then_some(ui::IconName::UserGroup)
3011    }
3012
3013    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
3014        Some("Collab Panel")
3015    }
3016
3017    fn toggle_action(&self) -> Box<dyn gpui::Action> {
3018        Box::new(ToggleFocus)
3019    }
3020
3021    fn persistent_name() -> &'static str {
3022        "CollabPanel"
3023    }
3024
3025    fn activation_priority(&self) -> u32 {
3026        6
3027    }
3028}
3029
3030impl Focusable for CollabPanel {
3031    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
3032        self.filter_editor.focus_handle(cx).clone()
3033    }
3034}
3035
3036impl PartialEq for ListEntry {
3037    fn eq(&self, other: &Self) -> bool {
3038        match self {
3039            ListEntry::Header(section_1) => {
3040                if let ListEntry::Header(section_2) = other {
3041                    return section_1 == section_2;
3042                }
3043            }
3044            ListEntry::CallParticipant { user: user_1, .. } => {
3045                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3046                    return user_1.id == user_2.id;
3047                }
3048            }
3049            ListEntry::ParticipantProject {
3050                project_id: project_id_1,
3051                ..
3052            } => {
3053                if let ListEntry::ParticipantProject {
3054                    project_id: project_id_2,
3055                    ..
3056                } = other
3057                {
3058                    return project_id_1 == project_id_2;
3059                }
3060            }
3061            ListEntry::ParticipantScreen {
3062                peer_id: peer_id_1, ..
3063            } => {
3064                if let ListEntry::ParticipantScreen {
3065                    peer_id: peer_id_2, ..
3066                } = other
3067                {
3068                    return peer_id_1 == peer_id_2;
3069                }
3070            }
3071            ListEntry::Channel {
3072                channel: channel_1, ..
3073            } => {
3074                if let ListEntry::Channel {
3075                    channel: channel_2, ..
3076                } = other
3077                {
3078                    return channel_1.id == channel_2.id;
3079                }
3080            }
3081            ListEntry::ChannelNotes { channel_id } => {
3082                if let ListEntry::ChannelNotes {
3083                    channel_id: other_id,
3084                } = other
3085                {
3086                    return channel_id == other_id;
3087                }
3088            }
3089            ListEntry::ChannelChat { channel_id } => {
3090                if let ListEntry::ChannelChat {
3091                    channel_id: other_id,
3092                } = other
3093                {
3094                    return channel_id == other_id;
3095                }
3096            }
3097            ListEntry::ChannelInvite(channel_1) => {
3098                if let ListEntry::ChannelInvite(channel_2) = other {
3099                    return channel_1.id == channel_2.id;
3100                }
3101            }
3102            ListEntry::IncomingRequest(user_1) => {
3103                if let ListEntry::IncomingRequest(user_2) = other {
3104                    return user_1.id == user_2.id;
3105                }
3106            }
3107            ListEntry::OutgoingRequest(user_1) => {
3108                if let ListEntry::OutgoingRequest(user_2) = other {
3109                    return user_1.id == user_2.id;
3110                }
3111            }
3112            ListEntry::Contact {
3113                contact: contact_1, ..
3114            } => {
3115                if let ListEntry::Contact {
3116                    contact: contact_2, ..
3117                } = other
3118                {
3119                    return contact_1.user.id == contact_2.user.id;
3120                }
3121            }
3122            ListEntry::ChannelEditor { depth } => {
3123                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3124                    return depth == other_depth;
3125                }
3126            }
3127            ListEntry::ContactPlaceholder => {
3128                if let ListEntry::ContactPlaceholder = other {
3129                    return true;
3130                }
3131            }
3132        }
3133        false
3134    }
3135}
3136
3137struct DraggedChannelView {
3138    channel: Channel,
3139    width: Pixels,
3140}
3141
3142impl Render for DraggedChannelView {
3143    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3144        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3145        h_flex()
3146            .font_family(ui_font)
3147            .bg(cx.theme().colors().background)
3148            .w(self.width)
3149            .p_1()
3150            .gap_1()
3151            .child(
3152                Icon::new(
3153                    if self.channel.visibility == proto::ChannelVisibility::Public {
3154                        IconName::Public
3155                    } else {
3156                        IconName::Hash
3157                    },
3158                )
3159                .size(IconSize::Small)
3160                .color(Color::Muted),
3161            )
3162            .child(Label::new(self.channel.name.clone()))
3163    }
3164}
3165
3166struct JoinChannelTooltip {
3167    channel_store: Entity<ChannelStore>,
3168    channel_id: ChannelId,
3169    #[allow(unused)]
3170    has_notes_notification: bool,
3171}
3172
3173impl Render for JoinChannelTooltip {
3174    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3175        tooltip_container(window, cx, |container, _, cx| {
3176            let participants = self
3177                .channel_store
3178                .read(cx)
3179                .channel_participants(self.channel_id);
3180
3181            container
3182                .child(Label::new("Join channel"))
3183                .children(participants.iter().map(|participant| {
3184                    h_flex()
3185                        .gap_2()
3186                        .child(Avatar::new(participant.avatar_uri.clone()))
3187                        .child(Label::new(participant.github_login.clone()))
3188                }))
3189        })
3190    }
3191}