collab_panel.rs

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