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