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, Indicator, 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 if role == proto::ChannelRole::Talker {
 858                Label::new("Mic only")
 859                    .color(Color::Muted)
 860                    .into_any_element()
 861            } else {
 862                div().into_any_element()
 863            })
 864            .when_some(peer_id, |el, peer_id| {
 865                if role == proto::ChannelRole::Guest {
 866                    return el;
 867                }
 868                el.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
 869                    .on_click(cx.listener(move |this, _, cx| {
 870                        this.workspace
 871                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx))
 872                            .ok();
 873                    }))
 874            })
 875            .when(is_call_admin, |el| {
 876                el.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
 877                    this.deploy_participant_context_menu(event.position, user_id, role, cx)
 878                }))
 879            })
 880    }
 881
 882    fn render_participant_project(
 883        &self,
 884        project_id: u64,
 885        worktree_root_names: &[String],
 886        host_user_id: u64,
 887        is_last: bool,
 888        is_selected: bool,
 889        cx: &mut ViewContext<Self>,
 890    ) -> impl IntoElement {
 891        let project_name: SharedString = if worktree_root_names.is_empty() {
 892            "untitled".to_string()
 893        } else {
 894            worktree_root_names.join(", ")
 895        }
 896        .into();
 897
 898        ListItem::new(project_id as usize)
 899            .selected(is_selected)
 900            .on_click(cx.listener(move |this, _, cx| {
 901                this.workspace
 902                    .update(cx, |workspace, cx| {
 903                        let app_state = workspace.app_state().clone();
 904                        workspace::join_remote_project(project_id, host_user_id, app_state, cx)
 905                            .detach_and_prompt_err("Failed to join project", cx, |_, _| None);
 906                    })
 907                    .ok();
 908            }))
 909            .start_slot(
 910                h_flex()
 911                    .gap_1()
 912                    .child(render_tree_branch(is_last, false, cx))
 913                    .child(IconButton::new(0, IconName::Folder)),
 914            )
 915            .child(Label::new(project_name.clone()))
 916            .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
 917    }
 918
 919    fn render_participant_screen(
 920        &self,
 921        peer_id: Option<PeerId>,
 922        is_last: bool,
 923        is_selected: bool,
 924        cx: &mut ViewContext<Self>,
 925    ) -> impl IntoElement {
 926        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
 927
 928        ListItem::new(("screen", id))
 929            .selected(is_selected)
 930            .start_slot(
 931                h_flex()
 932                    .gap_1()
 933                    .child(render_tree_branch(is_last, false, cx))
 934                    .child(IconButton::new(0, IconName::Screen)),
 935            )
 936            .child(Label::new("Screen"))
 937            .when_some(peer_id, |this, _| {
 938                this.on_click(cx.listener(move |this, _, cx| {
 939                    this.workspace
 940                        .update(cx, |workspace, cx| {
 941                            workspace.open_shared_screen(peer_id.unwrap(), cx)
 942                        })
 943                        .ok();
 944                }))
 945                .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
 946            })
 947    }
 948
 949    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
 950        if let Some(_) = self.channel_editing_state.take() {
 951            self.channel_name_editor.update(cx, |editor, cx| {
 952                editor.set_text("", cx);
 953            });
 954            true
 955        } else {
 956            false
 957        }
 958    }
 959
 960    fn render_channel_notes(
 961        &self,
 962        channel_id: ChannelId,
 963        is_selected: bool,
 964        cx: &mut ViewContext<Self>,
 965    ) -> impl IntoElement {
 966        let channel_store = self.channel_store.read(cx);
 967        let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
 968        ListItem::new("channel-notes")
 969            .selected(is_selected)
 970            .on_click(cx.listener(move |this, _, cx| {
 971                this.open_channel_notes(channel_id, cx);
 972            }))
 973            .start_slot(
 974                h_flex()
 975                    .relative()
 976                    .gap_1()
 977                    .child(render_tree_branch(false, true, cx))
 978                    .child(IconButton::new(0, IconName::File))
 979                    .children(has_channel_buffer_changed.then(|| {
 980                        div()
 981                            .w_1p5()
 982                            .z_index(1)
 983                            .absolute()
 984                            .right(px(2.))
 985                            .top(px(2.))
 986                            .child(Indicator::dot().color(Color::Info))
 987                    })),
 988            )
 989            .child(Label::new("notes"))
 990            .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
 991    }
 992
 993    fn render_channel_chat(
 994        &self,
 995        channel_id: ChannelId,
 996        is_selected: bool,
 997        cx: &mut ViewContext<Self>,
 998    ) -> impl IntoElement {
 999        let channel_store = self.channel_store.read(cx);
1000        let has_messages_notification = channel_store.has_new_messages(channel_id);
1001        ListItem::new("channel-chat")
1002            .selected(is_selected)
1003            .on_click(cx.listener(move |this, _, cx| {
1004                this.join_channel_chat(channel_id, cx);
1005            }))
1006            .start_slot(
1007                h_flex()
1008                    .relative()
1009                    .gap_1()
1010                    .child(render_tree_branch(false, false, cx))
1011                    .child(IconButton::new(0, IconName::MessageBubbles))
1012                    .children(has_messages_notification.then(|| {
1013                        div()
1014                            .w_1p5()
1015                            .z_index(1)
1016                            .absolute()
1017                            .right(px(2.))
1018                            .top(px(4.))
1019                            .child(Indicator::dot().color(Color::Info))
1020                    })),
1021            )
1022            .child(Label::new("chat"))
1023            .tooltip(move |cx| Tooltip::text("Open Chat", cx))
1024    }
1025
1026    fn has_subchannels(&self, ix: usize) -> bool {
1027        self.entries.get(ix).map_or(false, |entry| {
1028            if let ListEntry::Channel { has_children, .. } = entry {
1029                *has_children
1030            } else {
1031                false
1032            }
1033        })
1034    }
1035
1036    fn deploy_participant_context_menu(
1037        &mut self,
1038        position: Point<Pixels>,
1039        user_id: u64,
1040        role: proto::ChannelRole,
1041        cx: &mut ViewContext<Self>,
1042    ) {
1043        let this = cx.view().clone();
1044        if !(role == proto::ChannelRole::Guest
1045            || role == proto::ChannelRole::Talker
1046            || role == proto::ChannelRole::Member)
1047        {
1048            return;
1049        }
1050
1051        let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
1052            if role == proto::ChannelRole::Guest {
1053                context_menu = context_menu.entry(
1054                    "Grant Mic Access",
1055                    None,
1056                    cx.handler_for(&this, move |_, cx| {
1057                        ActiveCall::global(cx)
1058                            .update(cx, |call, cx| {
1059                                let Some(room) = call.room() else {
1060                                    return Task::ready(Ok(()));
1061                                };
1062                                room.update(cx, |room, cx| {
1063                                    room.set_participant_role(
1064                                        user_id,
1065                                        proto::ChannelRole::Talker,
1066                                        cx,
1067                                    )
1068                                })
1069                            })
1070                            .detach_and_prompt_err("Failed to grant mic access", cx, |_, _| None)
1071                    }),
1072                );
1073            }
1074            if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker {
1075                context_menu = context_menu.entry(
1076                    "Grant Write Access",
1077                    None,
1078                    cx.handler_for(&this, move |_, cx| {
1079                        ActiveCall::global(cx)
1080                            .update(cx, |call, cx| {
1081                                let Some(room) = call.room() else {
1082                                    return Task::ready(Ok(()));
1083                                };
1084                                room.update(cx, |room, cx| {
1085                                    room.set_participant_role(
1086                                        user_id,
1087                                        proto::ChannelRole::Member,
1088                                        cx,
1089                                    )
1090                                })
1091                            })
1092                            .detach_and_prompt_err("Failed to grant write access", cx, |e, _| {
1093                                match e.error_code() {
1094                                    ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()),
1095                                    _ => None,
1096                                }
1097                            })
1098                    }),
1099                );
1100            }
1101            if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker {
1102                let label = if role == proto::ChannelRole::Talker {
1103                    "Mute"
1104                } else {
1105                    "Revoke Access"
1106                };
1107                context_menu = context_menu.entry(
1108                    label,
1109                    None,
1110                    cx.handler_for(&this, move |_, cx| {
1111                        ActiveCall::global(cx)
1112                            .update(cx, |call, cx| {
1113                                let Some(room) = call.room() else {
1114                                    return Task::ready(Ok(()));
1115                                };
1116                                room.update(cx, |room, cx| {
1117                                    room.set_participant_role(
1118                                        user_id,
1119                                        proto::ChannelRole::Guest,
1120                                        cx,
1121                                    )
1122                                })
1123                            })
1124                            .detach_and_prompt_err("Failed to revoke access", cx, |_, _| None)
1125                    }),
1126                );
1127            }
1128
1129            context_menu
1130        });
1131
1132        cx.focus_view(&context_menu);
1133        let subscription =
1134            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1135                if this.context_menu.as_ref().is_some_and(|context_menu| {
1136                    context_menu.0.focus_handle(cx).contains_focused(cx)
1137                }) {
1138                    cx.focus_self();
1139                }
1140                this.context_menu.take();
1141                cx.notify();
1142            });
1143        self.context_menu = Some((context_menu, position, subscription));
1144    }
1145
1146    fn deploy_channel_context_menu(
1147        &mut self,
1148        position: Point<Pixels>,
1149        channel_id: ChannelId,
1150        ix: usize,
1151        cx: &mut ViewContext<Self>,
1152    ) {
1153        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
1154            self.channel_store
1155                .read(cx)
1156                .channel_for_id(clipboard.channel_id)
1157                .map(|channel| channel.name.clone())
1158        });
1159        let this = cx.view().clone();
1160
1161        let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
1162            if self.has_subchannels(ix) {
1163                let expand_action_name = if self.is_channel_collapsed(channel_id) {
1164                    "Expand Subchannels"
1165                } else {
1166                    "Collapse Subchannels"
1167                };
1168                context_menu = context_menu.entry(
1169                    expand_action_name,
1170                    None,
1171                    cx.handler_for(&this, move |this, cx| {
1172                        this.toggle_channel_collapsed(channel_id, cx)
1173                    }),
1174                );
1175            }
1176
1177            context_menu = context_menu
1178                .entry(
1179                    "Open Notes",
1180                    None,
1181                    cx.handler_for(&this, move |this, cx| {
1182                        this.open_channel_notes(channel_id, cx)
1183                    }),
1184                )
1185                .entry(
1186                    "Open Chat",
1187                    None,
1188                    cx.handler_for(&this, move |this, cx| {
1189                        this.join_channel_chat(channel_id, cx)
1190                    }),
1191                )
1192                .entry(
1193                    "Copy Channel Link",
1194                    None,
1195                    cx.handler_for(&this, move |this, cx| {
1196                        this.copy_channel_link(channel_id, cx)
1197                    }),
1198                );
1199
1200            let mut has_destructive_actions = false;
1201            if self.channel_store.read(cx).is_channel_admin(channel_id) {
1202                has_destructive_actions = true;
1203                context_menu = context_menu
1204                    .separator()
1205                    .entry(
1206                        "New Subchannel",
1207                        None,
1208                        cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
1209                    )
1210                    .entry(
1211                        "Rename",
1212                        Some(Box::new(SecondaryConfirm)),
1213                        cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
1214                    );
1215
1216                if let Some(channel_name) = clipboard_channel_name {
1217                    context_menu = context_menu.separator().entry(
1218                        format!("Move '#{}' here", channel_name),
1219                        None,
1220                        cx.handler_for(&this, move |this, cx| {
1221                            this.move_channel_on_clipboard(channel_id, cx)
1222                        }),
1223                    );
1224                }
1225
1226                if self.channel_store.read(cx).is_root_channel(channel_id) {
1227                    context_menu = context_menu.separator().entry(
1228                        "Manage Members",
1229                        None,
1230                        cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
1231                    )
1232                } else {
1233                    context_menu = context_menu.entry(
1234                        "Move this channel",
1235                        None,
1236                        cx.handler_for(&this, move |this, cx| {
1237                            this.start_move_channel(channel_id, cx)
1238                        }),
1239                    );
1240                    if self.channel_store.read(cx).is_public_channel(channel_id) {
1241                        context_menu = context_menu.separator().entry(
1242                            "Make Channel Private",
1243                            None,
1244                            cx.handler_for(&this, move |this, cx| {
1245                                this.set_channel_visibility(
1246                                    channel_id,
1247                                    ChannelVisibility::Members,
1248                                    cx,
1249                                )
1250                            }),
1251                        )
1252                    } else {
1253                        context_menu = context_menu.separator().entry(
1254                            "Make Channel Public",
1255                            None,
1256                            cx.handler_for(&this, move |this, cx| {
1257                                this.set_channel_visibility(
1258                                    channel_id,
1259                                    ChannelVisibility::Public,
1260                                    cx,
1261                                )
1262                            }),
1263                        )
1264                    }
1265                }
1266
1267                context_menu = context_menu.entry(
1268                    "Delete",
1269                    None,
1270                    cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
1271                );
1272            }
1273
1274            if self.channel_store.read(cx).is_root_channel(channel_id) {
1275                if !has_destructive_actions {
1276                    context_menu = context_menu.separator()
1277                }
1278                context_menu = context_menu.entry(
1279                    "Leave Channel",
1280                    None,
1281                    cx.handler_for(&this, move |this, cx| this.leave_channel(channel_id, cx)),
1282                );
1283            }
1284
1285            context_menu
1286        });
1287
1288        cx.focus_view(&context_menu);
1289        let subscription =
1290            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1291                if this.context_menu.as_ref().is_some_and(|context_menu| {
1292                    context_menu.0.focus_handle(cx).contains_focused(cx)
1293                }) {
1294                    cx.focus_self();
1295                }
1296                this.context_menu.take();
1297                cx.notify();
1298            });
1299        self.context_menu = Some((context_menu, position, subscription));
1300
1301        cx.notify();
1302    }
1303
1304    fn deploy_contact_context_menu(
1305        &mut self,
1306        position: Point<Pixels>,
1307        contact: Arc<Contact>,
1308        cx: &mut ViewContext<Self>,
1309    ) {
1310        let this = cx.view().clone();
1311        let in_room = ActiveCall::global(cx).read(cx).room().is_some();
1312
1313        let context_menu = ContextMenu::build(cx, |mut context_menu, _| {
1314            let user_id = contact.user.id;
1315
1316            if contact.online && !contact.busy {
1317                let label = if in_room {
1318                    format!("Invite {} to join", contact.user.github_login)
1319                } else {
1320                    format!("Call {}", contact.user.github_login)
1321                };
1322                context_menu = context_menu.entry(label, None, {
1323                    let this = this.clone();
1324                    move |cx| {
1325                        this.update(cx, |this, cx| {
1326                            this.call(user_id, cx);
1327                        });
1328                    }
1329                });
1330            }
1331
1332            context_menu.entry("Remove Contact", None, {
1333                let this = this.clone();
1334                move |cx| {
1335                    this.update(cx, |this, cx| {
1336                        this.remove_contact(contact.user.id, &contact.user.github_login, cx);
1337                    });
1338                }
1339            })
1340        });
1341
1342        cx.focus_view(&context_menu);
1343        let subscription =
1344            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1345                if this.context_menu.as_ref().is_some_and(|context_menu| {
1346                    context_menu.0.focus_handle(cx).contains_focused(cx)
1347                }) {
1348                    cx.focus_self();
1349                }
1350                this.context_menu.take();
1351                cx.notify();
1352            });
1353        self.context_menu = Some((context_menu, position, subscription));
1354
1355        cx.notify();
1356    }
1357
1358    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1359        if self.take_editing_state(cx) {
1360            cx.focus_view(&self.filter_editor);
1361        } else {
1362            self.filter_editor.update(cx, |editor, cx| {
1363                if editor.buffer().read(cx).len(cx) > 0 {
1364                    editor.set_text("", cx);
1365                }
1366            });
1367        }
1368
1369        self.update_entries(false, cx);
1370    }
1371
1372    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1373        let ix = self.selection.map_or(0, |ix| ix + 1);
1374        if ix < self.entries.len() {
1375            self.selection = Some(ix);
1376        }
1377
1378        if let Some(ix) = self.selection {
1379            self.scroll_to_item(ix)
1380        }
1381        cx.notify();
1382    }
1383
1384    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1385        let ix = self.selection.take().unwrap_or(0);
1386        if ix > 0 {
1387            self.selection = Some(ix - 1);
1388        }
1389
1390        if let Some(ix) = self.selection {
1391            self.scroll_to_item(ix)
1392        }
1393        cx.notify();
1394    }
1395
1396    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1397        if self.confirm_channel_edit(cx) {
1398            return;
1399        }
1400
1401        if let Some(selection) = self.selection {
1402            if let Some(entry) = self.entries.get(selection) {
1403                match entry {
1404                    ListEntry::Header(section) => match section {
1405                        Section::ActiveCall => Self::leave_call(cx),
1406                        Section::Channels => self.new_root_channel(cx),
1407                        Section::Contacts => self.toggle_contact_finder(cx),
1408                        Section::ContactRequests
1409                        | Section::Online
1410                        | Section::Offline
1411                        | Section::ChannelInvites => {
1412                            self.toggle_section_expanded(*section, cx);
1413                        }
1414                    },
1415                    ListEntry::Contact { contact, calling } => {
1416                        if contact.online && !contact.busy && !calling {
1417                            self.call(contact.user.id, cx);
1418                        }
1419                    }
1420                    ListEntry::ParticipantProject {
1421                        project_id,
1422                        host_user_id,
1423                        ..
1424                    } => {
1425                        if let Some(workspace) = self.workspace.upgrade() {
1426                            let app_state = workspace.read(cx).app_state().clone();
1427                            workspace::join_remote_project(
1428                                *project_id,
1429                                *host_user_id,
1430                                app_state,
1431                                cx,
1432                            )
1433                            .detach_and_prompt_err(
1434                                "Failed to join project",
1435                                cx,
1436                                |_, _| None,
1437                            );
1438                        }
1439                    }
1440                    ListEntry::ParticipantScreen { peer_id, .. } => {
1441                        let Some(peer_id) = peer_id else {
1442                            return;
1443                        };
1444                        if let Some(workspace) = self.workspace.upgrade() {
1445                            workspace.update(cx, |workspace, cx| {
1446                                workspace.open_shared_screen(*peer_id, cx)
1447                            });
1448                        }
1449                    }
1450                    ListEntry::Channel { channel, .. } => {
1451                        let is_active = maybe!({
1452                            let call_channel = ActiveCall::global(cx)
1453                                .read(cx)
1454                                .room()?
1455                                .read(cx)
1456                                .channel_id()?;
1457
1458                            Some(call_channel == channel.id)
1459                        })
1460                        .unwrap_or(false);
1461                        if is_active {
1462                            self.open_channel_notes(channel.id, cx)
1463                        } else {
1464                            self.join_channel(channel.id, cx)
1465                        }
1466                    }
1467                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
1468                    ListEntry::CallParticipant { user, peer_id, .. } => {
1469                        if Some(user) == self.user_store.read(cx).current_user().as_ref() {
1470                            Self::leave_call(cx);
1471                        } else if let Some(peer_id) = peer_id {
1472                            self.workspace
1473                                .update(cx, |workspace, cx| workspace.follow(*peer_id, cx))
1474                                .ok();
1475                        }
1476                    }
1477                    ListEntry::IncomingRequest(user) => {
1478                        self.respond_to_contact_request(user.id, true, cx)
1479                    }
1480                    ListEntry::ChannelInvite(channel) => {
1481                        self.respond_to_channel_invite(channel.id, true, cx)
1482                    }
1483                    ListEntry::ChannelNotes { channel_id } => {
1484                        self.open_channel_notes(*channel_id, cx)
1485                    }
1486                    ListEntry::ChannelChat { channel_id } => {
1487                        self.join_channel_chat(*channel_id, cx)
1488                    }
1489
1490                    ListEntry::OutgoingRequest(_) => {}
1491                    ListEntry::ChannelEditor { .. } => {}
1492                }
1493            }
1494        }
1495    }
1496
1497    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
1498        if self.channel_editing_state.is_some() {
1499            self.channel_name_editor.update(cx, |editor, cx| {
1500                editor.insert(" ", cx);
1501            });
1502        }
1503    }
1504
1505    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
1506        if let Some(editing_state) = &mut self.channel_editing_state {
1507            match editing_state {
1508                ChannelEditingState::Create {
1509                    location,
1510                    pending_name,
1511                    ..
1512                } => {
1513                    if pending_name.is_some() {
1514                        return false;
1515                    }
1516                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1517
1518                    *pending_name = Some(channel_name.clone());
1519
1520                    self.channel_store
1521                        .update(cx, |channel_store, cx| {
1522                            channel_store.create_channel(&channel_name, *location, cx)
1523                        })
1524                        .detach();
1525                    cx.notify();
1526                }
1527                ChannelEditingState::Rename {
1528                    location,
1529                    pending_name,
1530                } => {
1531                    if pending_name.is_some() {
1532                        return false;
1533                    }
1534                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1535                    *pending_name = Some(channel_name.clone());
1536
1537                    self.channel_store
1538                        .update(cx, |channel_store, cx| {
1539                            channel_store.rename(*location, &channel_name, cx)
1540                        })
1541                        .detach();
1542                    cx.notify();
1543                }
1544            }
1545            cx.focus_self();
1546            true
1547        } else {
1548            false
1549        }
1550    }
1551
1552    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1553        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1554            self.collapsed_sections.remove(ix);
1555        } else {
1556            self.collapsed_sections.push(section);
1557        }
1558        self.update_entries(false, cx);
1559    }
1560
1561    fn collapse_selected_channel(
1562        &mut self,
1563        _: &CollapseSelectedChannel,
1564        cx: &mut ViewContext<Self>,
1565    ) {
1566        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1567            return;
1568        };
1569
1570        if self.is_channel_collapsed(channel_id) {
1571            return;
1572        }
1573
1574        self.toggle_channel_collapsed(channel_id, cx);
1575    }
1576
1577    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
1578        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1579            return;
1580        };
1581
1582        if !self.is_channel_collapsed(id) {
1583            return;
1584        }
1585
1586        self.toggle_channel_collapsed(id, cx)
1587    }
1588
1589    fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1590        match self.collapsed_channels.binary_search(&channel_id) {
1591            Ok(ix) => {
1592                self.collapsed_channels.remove(ix);
1593            }
1594            Err(ix) => {
1595                self.collapsed_channels.insert(ix, channel_id);
1596            }
1597        };
1598        self.serialize(cx);
1599        self.update_entries(true, cx);
1600        cx.notify();
1601        cx.focus_self();
1602    }
1603
1604    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1605        self.collapsed_channels.binary_search(&channel_id).is_ok()
1606    }
1607
1608    fn leave_call(cx: &mut WindowContext) {
1609        ActiveCall::global(cx)
1610            .update(cx, |call, cx| call.hang_up(cx))
1611            .detach_and_prompt_err("Failed to hang up", cx, |_, _| None);
1612    }
1613
1614    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1615        if let Some(workspace) = self.workspace.upgrade() {
1616            workspace.update(cx, |workspace, cx| {
1617                workspace.toggle_modal(cx, |cx| {
1618                    let mut finder = ContactFinder::new(self.user_store.clone(), cx);
1619                    finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1620                    finder
1621                });
1622            });
1623        }
1624    }
1625
1626    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
1627        self.channel_editing_state = Some(ChannelEditingState::Create {
1628            location: None,
1629            pending_name: None,
1630        });
1631        self.update_entries(false, cx);
1632        self.select_channel_editor();
1633        cx.focus_view(&self.channel_name_editor);
1634        cx.notify();
1635    }
1636
1637    fn select_channel_editor(&mut self) {
1638        self.selection = self.entries.iter().position(|entry| match entry {
1639            ListEntry::ChannelEditor { .. } => true,
1640            _ => false,
1641        });
1642    }
1643
1644    fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1645        self.collapsed_channels
1646            .retain(|channel| *channel != channel_id);
1647        self.channel_editing_state = Some(ChannelEditingState::Create {
1648            location: Some(channel_id),
1649            pending_name: None,
1650        });
1651        self.update_entries(false, cx);
1652        self.select_channel_editor();
1653        cx.focus_view(&self.channel_name_editor);
1654        cx.notify();
1655    }
1656
1657    fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1658        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
1659    }
1660
1661    fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
1662        if let Some(channel) = self.selected_channel() {
1663            self.remove_channel(channel.id, cx)
1664        }
1665    }
1666
1667    fn rename_selected_channel(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext<Self>) {
1668        if let Some(channel) = self.selected_channel() {
1669            self.rename_channel(channel.id, cx);
1670        }
1671    }
1672
1673    fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1674        let channel_store = self.channel_store.read(cx);
1675        if !channel_store.is_channel_admin(channel_id) {
1676            return;
1677        }
1678        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
1679            self.channel_editing_state = Some(ChannelEditingState::Rename {
1680                location: channel_id,
1681                pending_name: None,
1682            });
1683            self.channel_name_editor.update(cx, |editor, cx| {
1684                editor.set_text(channel.name.clone(), cx);
1685                editor.select_all(&Default::default(), cx);
1686            });
1687            cx.focus_view(&self.channel_name_editor);
1688            self.update_entries(false, cx);
1689            self.select_channel_editor();
1690        }
1691    }
1692
1693    fn set_channel_visibility(
1694        &mut self,
1695        channel_id: ChannelId,
1696        visibility: ChannelVisibility,
1697        cx: &mut ViewContext<Self>,
1698    ) {
1699        self.channel_store
1700            .update(cx, |channel_store, cx| {
1701                channel_store.set_channel_visibility(channel_id, visibility, cx)
1702            })
1703            .detach_and_prompt_err("Failed to set channel visibility", cx, |e, _| match e.error_code() {
1704                ErrorCode::BadPublicNesting =>
1705                    if e.error_tag("direction") == Some("parent") {
1706                        Some("To make a channel public, its parent channel must be public.".to_string())
1707                    } else {
1708                        Some("To make a channel private, all of its subchannels must be private.".to_string())
1709                    },
1710                _ => None
1711            });
1712    }
1713
1714    fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
1715        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
1716    }
1717
1718    fn start_move_selected_channel(&mut self, _: &StartMoveChannel, cx: &mut ViewContext<Self>) {
1719        if let Some(channel) = self.selected_channel() {
1720            self.start_move_channel(channel.id, cx);
1721        }
1722    }
1723
1724    fn move_channel_on_clipboard(
1725        &mut self,
1726        to_channel_id: ChannelId,
1727        cx: &mut ViewContext<CollabPanel>,
1728    ) {
1729        if let Some(clipboard) = self.channel_clipboard.take() {
1730            self.move_channel(clipboard.channel_id, to_channel_id, cx)
1731        }
1732    }
1733
1734    fn move_channel(&self, channel_id: ChannelId, to: ChannelId, cx: &mut ViewContext<Self>) {
1735        self.channel_store
1736            .update(cx, |channel_store, cx| {
1737                channel_store.move_channel(channel_id, to, cx)
1738            })
1739            .detach_and_prompt_err("Failed to move channel", cx, |e, _| match e.error_code() {
1740                ErrorCode::BadPublicNesting => {
1741                    Some("Public channels must have public parents".into())
1742                }
1743                ErrorCode::CircularNesting => Some("You cannot move a channel into itself".into()),
1744                ErrorCode::WrongMoveTarget => {
1745                    Some("You cannot move a channel into a different root channel".into())
1746                }
1747                _ => None,
1748            })
1749    }
1750
1751    fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1752        if let Some(workspace) = self.workspace.upgrade() {
1753            ChannelView::open(channel_id, None, workspace, cx).detach();
1754        }
1755    }
1756
1757    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
1758        let Some(bounds) = self
1759            .selection
1760            .and_then(|ix| self.list_state.bounds_for_item(ix))
1761        else {
1762            return;
1763        };
1764
1765        if let Some(channel) = self.selected_channel() {
1766            self.deploy_channel_context_menu(
1767                bounds.center(),
1768                channel.id,
1769                self.selection.unwrap(),
1770                cx,
1771            );
1772            cx.stop_propagation();
1773            return;
1774        };
1775
1776        if let Some(contact) = self.selected_contact() {
1777            self.deploy_contact_context_menu(bounds.center(), contact, cx);
1778            cx.stop_propagation();
1779            return;
1780        };
1781    }
1782
1783    fn selected_channel(&self) -> Option<&Arc<Channel>> {
1784        self.selection
1785            .and_then(|ix| self.entries.get(ix))
1786            .and_then(|entry| match entry {
1787                ListEntry::Channel { channel, .. } => Some(channel),
1788                _ => None,
1789            })
1790    }
1791
1792    fn selected_contact(&self) -> Option<Arc<Contact>> {
1793        self.selection
1794            .and_then(|ix| self.entries.get(ix))
1795            .and_then(|entry| match entry {
1796                ListEntry::Contact { contact, .. } => Some(contact.clone()),
1797                _ => None,
1798            })
1799    }
1800
1801    fn show_channel_modal(
1802        &mut self,
1803        channel_id: ChannelId,
1804        mode: channel_modal::Mode,
1805        cx: &mut ViewContext<Self>,
1806    ) {
1807        let workspace = self.workspace.clone();
1808        let user_store = self.user_store.clone();
1809        let channel_store = self.channel_store.clone();
1810        let members = self.channel_store.update(cx, |channel_store, cx| {
1811            channel_store.get_channel_member_details(channel_id, cx)
1812        });
1813
1814        cx.spawn(|_, mut cx| async move {
1815            let members = members.await?;
1816            workspace.update(&mut cx, |workspace, cx| {
1817                workspace.toggle_modal(cx, |cx| {
1818                    ChannelModal::new(
1819                        user_store.clone(),
1820                        channel_store.clone(),
1821                        channel_id,
1822                        mode,
1823                        members,
1824                        cx,
1825                    )
1826                });
1827            })
1828        })
1829        .detach();
1830    }
1831
1832    fn leave_channel(&self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1833        let Some(user_id) = self.user_store.read(cx).current_user().map(|u| u.id) else {
1834            return;
1835        };
1836        let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) else {
1837            return;
1838        };
1839        let prompt_message = format!("Are you sure you want to leave \"#{}\"?", channel.name);
1840        let answer = cx.prompt(
1841            PromptLevel::Warning,
1842            &prompt_message,
1843            None,
1844            &["Leave", "Cancel"],
1845        );
1846        cx.spawn(|this, mut cx| async move {
1847            if answer.await? != 0 {
1848                return Ok(());
1849            }
1850            this.update(&mut cx, |this, cx| {
1851                this.channel_store.update(cx, |channel_store, cx| {
1852                    channel_store.remove_member(channel_id, user_id, cx)
1853                })
1854            })?
1855            .await
1856        })
1857        .detach_and_prompt_err("Failed to leave channel", cx, |_, _| None)
1858    }
1859
1860    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1861        let channel_store = self.channel_store.clone();
1862        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
1863            let prompt_message = format!(
1864                "Are you sure you want to remove the channel \"{}\"?",
1865                channel.name
1866            );
1867            let answer = cx.prompt(
1868                PromptLevel::Warning,
1869                &prompt_message,
1870                None,
1871                &["Remove", "Cancel"],
1872            );
1873            cx.spawn(|this, mut cx| async move {
1874                if answer.await? == 0 {
1875                    channel_store
1876                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
1877                        .await
1878                        .notify_async_err(&mut cx);
1879                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
1880                }
1881                anyhow::Ok(())
1882            })
1883            .detach();
1884        }
1885    }
1886
1887    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
1888        let user_store = self.user_store.clone();
1889        let prompt_message = format!(
1890            "Are you sure you want to remove \"{}\" from your contacts?",
1891            github_login
1892        );
1893        let answer = cx.prompt(
1894            PromptLevel::Warning,
1895            &prompt_message,
1896            None,
1897            &["Remove", "Cancel"],
1898        );
1899        cx.spawn(|_, mut cx| async move {
1900            if answer.await? == 0 {
1901                user_store
1902                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
1903                    .await
1904                    .notify_async_err(&mut cx);
1905            }
1906            anyhow::Ok(())
1907        })
1908        .detach_and_prompt_err("Failed to remove contact", cx, |_, _| None);
1909    }
1910
1911    fn respond_to_contact_request(
1912        &mut self,
1913        user_id: u64,
1914        accept: bool,
1915        cx: &mut ViewContext<Self>,
1916    ) {
1917        self.user_store
1918            .update(cx, |store, cx| {
1919                store.respond_to_contact_request(user_id, accept, cx)
1920            })
1921            .detach_and_prompt_err("Failed to respond to contact request", cx, |_, _| None);
1922    }
1923
1924    fn respond_to_channel_invite(
1925        &mut self,
1926        channel_id: u64,
1927        accept: bool,
1928        cx: &mut ViewContext<Self>,
1929    ) {
1930        self.channel_store
1931            .update(cx, |store, cx| {
1932                store.respond_to_channel_invite(channel_id, accept, cx)
1933            })
1934            .detach();
1935    }
1936
1937    fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext<Self>) {
1938        ActiveCall::global(cx)
1939            .update(cx, |call, cx| {
1940                call.invite(recipient_user_id, Some(self.project.clone()), cx)
1941            })
1942            .detach_and_prompt_err("Call failed", cx, |_, _| None);
1943    }
1944
1945    fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
1946        let Some(workspace) = self.workspace.upgrade() else {
1947            return;
1948        };
1949        let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
1950            return;
1951        };
1952        workspace::join_channel(
1953            channel_id,
1954            workspace.read(cx).app_state().clone(),
1955            Some(handle),
1956            cx,
1957        )
1958        .detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
1959    }
1960
1961    fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1962        let Some(workspace) = self.workspace.upgrade() else {
1963            return;
1964        };
1965        cx.window_context().defer(move |cx| {
1966            workspace.update(cx, |workspace, cx| {
1967                if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
1968                    panel.update(cx, |panel, cx| {
1969                        panel
1970                            .select_channel(channel_id, None, cx)
1971                            .detach_and_notify_err(cx);
1972                    });
1973                }
1974            });
1975        });
1976    }
1977
1978    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1979        let channel_store = self.channel_store.read(cx);
1980        let Some(channel) = channel_store.channel_for_id(channel_id) else {
1981            return;
1982        };
1983        let item = ClipboardItem::new(channel.link());
1984        cx.write_to_clipboard(item)
1985    }
1986
1987    fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
1988        let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
1989
1990        v_flex()
1991            .gap_6()
1992            .p_4()
1993            .child(Label::new(collab_blurb))
1994            .child(
1995                v_flex()
1996                    .gap_2()
1997                    .child(
1998                        Button::new("sign_in", "Sign in")
1999                            .icon_color(Color::Muted)
2000                            .icon(IconName::Github)
2001                            .icon_position(IconPosition::Start)
2002                            .style(ButtonStyle::Filled)
2003                            .full_width()
2004                            .on_click(cx.listener(|this, _, cx| {
2005                                let client = this.client.clone();
2006                                cx.spawn(|_, mut cx| async move {
2007                                    client
2008                                        .authenticate_and_connect(true, &cx)
2009                                        .await
2010                                        .notify_async_err(&mut cx);
2011                                })
2012                                .detach()
2013                            })),
2014                    )
2015                    .child(
2016                        div().flex().w_full().items_center().child(
2017                            Label::new("Sign in to enable collaboration.")
2018                                .color(Color::Muted)
2019                                .size(LabelSize::Small),
2020                        ),
2021                    ),
2022            )
2023    }
2024
2025    fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
2026        let entry = &self.entries[ix];
2027
2028        let is_selected = self.selection == Some(ix);
2029        match entry {
2030            ListEntry::Header(section) => {
2031                let is_collapsed = self.collapsed_sections.contains(section);
2032                self.render_header(*section, is_selected, is_collapsed, cx)
2033                    .into_any_element()
2034            }
2035            ListEntry::Contact { contact, calling } => self
2036                .render_contact(contact, *calling, is_selected, cx)
2037                .into_any_element(),
2038            ListEntry::ContactPlaceholder => self
2039                .render_contact_placeholder(is_selected, cx)
2040                .into_any_element(),
2041            ListEntry::IncomingRequest(user) => self
2042                .render_contact_request(user, true, is_selected, cx)
2043                .into_any_element(),
2044            ListEntry::OutgoingRequest(user) => self
2045                .render_contact_request(user, false, is_selected, cx)
2046                .into_any_element(),
2047            ListEntry::Channel {
2048                channel,
2049                depth,
2050                has_children,
2051            } => self
2052                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
2053                .into_any_element(),
2054            ListEntry::ChannelEditor { depth } => {
2055                self.render_channel_editor(*depth, cx).into_any_element()
2056            }
2057            ListEntry::ChannelInvite(channel) => self
2058                .render_channel_invite(channel, is_selected, cx)
2059                .into_any_element(),
2060            ListEntry::CallParticipant {
2061                user,
2062                peer_id,
2063                is_pending,
2064                role,
2065            } => self
2066                .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
2067                .into_any_element(),
2068            ListEntry::ParticipantProject {
2069                project_id,
2070                worktree_root_names,
2071                host_user_id,
2072                is_last,
2073            } => self
2074                .render_participant_project(
2075                    *project_id,
2076                    &worktree_root_names,
2077                    *host_user_id,
2078                    *is_last,
2079                    is_selected,
2080                    cx,
2081                )
2082                .into_any_element(),
2083            ListEntry::ParticipantScreen { peer_id, is_last } => self
2084                .render_participant_screen(*peer_id, *is_last, is_selected, cx)
2085                .into_any_element(),
2086            ListEntry::ChannelNotes { channel_id } => self
2087                .render_channel_notes(*channel_id, is_selected, cx)
2088                .into_any_element(),
2089            ListEntry::ChannelChat { channel_id } => self
2090                .render_channel_chat(*channel_id, is_selected, cx)
2091                .into_any_element(),
2092        }
2093    }
2094
2095    fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
2096        v_flex()
2097            .size_full()
2098            .child(list(self.list_state.clone()).size_full())
2099            .child(
2100                v_flex()
2101                    .child(div().mx_2().border_primary(cx).border_t())
2102                    .child(
2103                        v_flex()
2104                            .p_2()
2105                            .child(self.render_filter_input(&self.filter_editor, cx)),
2106                    ),
2107            )
2108    }
2109
2110    fn render_filter_input(
2111        &self,
2112        editor: &View<Editor>,
2113        cx: &mut ViewContext<Self>,
2114    ) -> impl IntoElement {
2115        let settings = ThemeSettings::get_global(cx);
2116        let text_style = TextStyle {
2117            color: if editor.read(cx).read_only(cx) {
2118                cx.theme().colors().text_disabled
2119            } else {
2120                cx.theme().colors().text
2121            },
2122            font_family: settings.ui_font.family.clone(),
2123            font_features: settings.ui_font.features,
2124            font_size: rems(0.875).into(),
2125            font_weight: FontWeight::NORMAL,
2126            font_style: FontStyle::Normal,
2127            line_height: relative(1.3).into(),
2128            background_color: None,
2129            underline: None,
2130            strikethrough: None,
2131            white_space: WhiteSpace::Normal,
2132        };
2133
2134        EditorElement::new(
2135            editor,
2136            EditorStyle {
2137                local_player: cx.theme().players().local(),
2138                text: text_style,
2139                ..Default::default()
2140            },
2141        )
2142    }
2143
2144    fn render_header(
2145        &self,
2146        section: Section,
2147        is_selected: bool,
2148        is_collapsed: bool,
2149        cx: &ViewContext<Self>,
2150    ) -> impl IntoElement {
2151        let mut channel_link = None;
2152        let mut channel_tooltip_text = None;
2153        let mut channel_icon = None;
2154
2155        let text = match section {
2156            Section::ActiveCall => {
2157                let channel_name = maybe!({
2158                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2159
2160                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2161
2162                    channel_link = Some(channel.link());
2163                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2164                        proto::ChannelVisibility::Public => {
2165                            (Some("icons/public.svg"), Some("Copy public channel link."))
2166                        }
2167                        proto::ChannelVisibility::Members => {
2168                            (Some("icons/hash.svg"), Some("Copy private channel link."))
2169                        }
2170                    };
2171
2172                    Some(channel.name.as_ref())
2173                });
2174
2175                if let Some(name) = channel_name {
2176                    SharedString::from(format!("{}", name))
2177                } else {
2178                    SharedString::from("Current Call")
2179                }
2180            }
2181            Section::ContactRequests => SharedString::from("Requests"),
2182            Section::Contacts => SharedString::from("Contacts"),
2183            Section::Channels => SharedString::from("Channels"),
2184            Section::ChannelInvites => SharedString::from("Invites"),
2185            Section::Online => SharedString::from("Online"),
2186            Section::Offline => SharedString::from("Offline"),
2187        };
2188
2189        let button = match section {
2190            Section::ActiveCall => channel_link.map(|channel_link| {
2191                let channel_link_copy = channel_link.clone();
2192                IconButton::new("channel-link", IconName::Copy)
2193                    .icon_size(IconSize::Small)
2194                    .size(ButtonSize::None)
2195                    .visible_on_hover("section-header")
2196                    .on_click(move |_, cx| {
2197                        let item = ClipboardItem::new(channel_link_copy.clone());
2198                        cx.write_to_clipboard(item)
2199                    })
2200                    .tooltip(|cx| Tooltip::text("Copy channel link", cx))
2201                    .into_any_element()
2202            }),
2203            Section::Contacts => Some(
2204                IconButton::new("add-contact", IconName::Plus)
2205                    .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2206                    .tooltip(|cx| Tooltip::text("Search for new contact", cx))
2207                    .into_any_element(),
2208            ),
2209            Section::Channels => Some(
2210                IconButton::new("add-channel", IconName::Plus)
2211                    .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
2212                    .tooltip(|cx| Tooltip::text("Create a channel", cx))
2213                    .into_any_element(),
2214            ),
2215            _ => None,
2216        };
2217
2218        let can_collapse = match section {
2219            Section::ActiveCall | Section::Channels | Section::Contacts => false,
2220            Section::ChannelInvites
2221            | Section::ContactRequests
2222            | Section::Online
2223            | Section::Offline => true,
2224        };
2225
2226        h_flex().w_full().group("section-header").child(
2227            ListHeader::new(text)
2228                .when(can_collapse, |header| {
2229                    header
2230                        .toggle(Some(!is_collapsed))
2231                        .on_toggle(cx.listener(move |this, _, cx| {
2232                            this.toggle_section_expanded(section, cx);
2233                        }))
2234                })
2235                .inset(true)
2236                .end_slot::<AnyElement>(button)
2237                .selected(is_selected),
2238        )
2239    }
2240
2241    fn render_contact(
2242        &self,
2243        contact: &Arc<Contact>,
2244        calling: bool,
2245        is_selected: bool,
2246        cx: &mut ViewContext<Self>,
2247    ) -> impl IntoElement {
2248        let online = contact.online;
2249        let busy = contact.busy || calling;
2250        let github_login = SharedString::from(contact.user.github_login.clone());
2251        let item = ListItem::new(github_login.clone())
2252            .indent_level(1)
2253            .indent_step_size(px(20.))
2254            .selected(is_selected)
2255            .child(
2256                h_flex()
2257                    .w_full()
2258                    .justify_between()
2259                    .child(Label::new(github_login.clone()))
2260                    .when(calling, |el| {
2261                        el.child(Label::new("Calling").color(Color::Muted))
2262                    })
2263                    .when(!calling, |el| {
2264                        el.child(
2265                            IconButton::new("contact context menu", IconName::Ellipsis)
2266                                .icon_color(Color::Muted)
2267                                .visible_on_hover("")
2268                                .on_click(cx.listener({
2269                                    let contact = contact.clone();
2270                                    move |this, event: &ClickEvent, cx| {
2271                                        this.deploy_contact_context_menu(
2272                                            event.down.position,
2273                                            contact.clone(),
2274                                            cx,
2275                                        );
2276                                    }
2277                                })),
2278                        )
2279                    }),
2280            )
2281            .on_secondary_mouse_down(cx.listener({
2282                let contact = contact.clone();
2283                move |this, event: &MouseDownEvent, cx| {
2284                    this.deploy_contact_context_menu(event.position, contact.clone(), cx);
2285                }
2286            }))
2287            .start_slot(
2288                // todo handle contacts with no avatar
2289                Avatar::new(contact.user.avatar_uri.clone())
2290                    .indicator::<AvatarAvailabilityIndicator>(if online {
2291                        Some(AvatarAvailabilityIndicator::new(match busy {
2292                            true => ui::Availability::Busy,
2293                            false => ui::Availability::Free,
2294                        }))
2295                    } else {
2296                        None
2297                    }),
2298            );
2299
2300        div()
2301            .id(github_login.clone())
2302            .group("")
2303            .child(item)
2304            .tooltip(move |cx| {
2305                let text = if !online {
2306                    format!(" {} is offline", &github_login)
2307                } else if busy {
2308                    format!(" {} is on a call", &github_login)
2309                } else {
2310                    let room = ActiveCall::global(cx).read(cx).room();
2311                    if room.is_some() {
2312                        format!("Invite {} to join call", &github_login)
2313                    } else {
2314                        format!("Call {}", &github_login)
2315                    }
2316                };
2317                Tooltip::text(text, cx)
2318            })
2319    }
2320
2321    fn render_contact_request(
2322        &self,
2323        user: &Arc<User>,
2324        is_incoming: bool,
2325        is_selected: bool,
2326        cx: &mut ViewContext<Self>,
2327    ) -> impl IntoElement {
2328        let github_login = SharedString::from(user.github_login.clone());
2329        let user_id = user.id;
2330        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2331        let color = if is_response_pending {
2332            Color::Muted
2333        } else {
2334            Color::Default
2335        };
2336
2337        let controls = if is_incoming {
2338            vec![
2339                IconButton::new("decline-contact", IconName::Close)
2340                    .on_click(cx.listener(move |this, _, cx| {
2341                        this.respond_to_contact_request(user_id, false, cx);
2342                    }))
2343                    .icon_color(color)
2344                    .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2345                IconButton::new("accept-contact", IconName::Check)
2346                    .on_click(cx.listener(move |this, _, cx| {
2347                        this.respond_to_contact_request(user_id, true, cx);
2348                    }))
2349                    .icon_color(color)
2350                    .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2351            ]
2352        } else {
2353            let github_login = github_login.clone();
2354            vec![IconButton::new("remove_contact", IconName::Close)
2355                .on_click(cx.listener(move |this, _, cx| {
2356                    this.remove_contact(user_id, &github_login, cx);
2357                }))
2358                .icon_color(color)
2359                .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2360        };
2361
2362        ListItem::new(github_login.clone())
2363            .indent_level(1)
2364            .indent_step_size(px(20.))
2365            .selected(is_selected)
2366            .child(
2367                h_flex()
2368                    .w_full()
2369                    .justify_between()
2370                    .child(Label::new(github_login.clone()))
2371                    .child(h_flex().children(controls)),
2372            )
2373            .start_slot(Avatar::new(user.avatar_uri.clone()))
2374    }
2375
2376    fn render_channel_invite(
2377        &self,
2378        channel: &Arc<Channel>,
2379        is_selected: bool,
2380        cx: &mut ViewContext<Self>,
2381    ) -> ListItem {
2382        let channel_id = channel.id;
2383        let response_is_pending = self
2384            .channel_store
2385            .read(cx)
2386            .has_pending_channel_invite_response(&channel);
2387        let color = if response_is_pending {
2388            Color::Muted
2389        } else {
2390            Color::Default
2391        };
2392
2393        let controls = [
2394            IconButton::new("reject-invite", IconName::Close)
2395                .on_click(cx.listener(move |this, _, cx| {
2396                    this.respond_to_channel_invite(channel_id, false, cx);
2397                }))
2398                .icon_color(color)
2399                .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2400            IconButton::new("accept-invite", IconName::Check)
2401                .on_click(cx.listener(move |this, _, cx| {
2402                    this.respond_to_channel_invite(channel_id, true, cx);
2403                }))
2404                .icon_color(color)
2405                .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2406        ];
2407
2408        ListItem::new(("channel-invite", channel.id as usize))
2409            .selected(is_selected)
2410            .child(
2411                h_flex()
2412                    .w_full()
2413                    .justify_between()
2414                    .child(Label::new(channel.name.clone()))
2415                    .child(h_flex().children(controls)),
2416            )
2417            .start_slot(
2418                Icon::new(IconName::Hash)
2419                    .size(IconSize::Small)
2420                    .color(Color::Muted),
2421            )
2422    }
2423
2424    fn render_contact_placeholder(
2425        &self,
2426        is_selected: bool,
2427        cx: &mut ViewContext<Self>,
2428    ) -> ListItem {
2429        ListItem::new("contact-placeholder")
2430            .child(Icon::new(IconName::Plus))
2431            .child(Label::new("Add a Contact"))
2432            .selected(is_selected)
2433            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2434    }
2435
2436    fn render_channel(
2437        &self,
2438        channel: &Channel,
2439        depth: usize,
2440        has_children: bool,
2441        is_selected: bool,
2442        ix: usize,
2443        cx: &mut ViewContext<Self>,
2444    ) -> impl IntoElement {
2445        let channel_id = channel.id;
2446
2447        let is_active = maybe!({
2448            let call_channel = ActiveCall::global(cx)
2449                .read(cx)
2450                .room()?
2451                .read(cx)
2452                .channel_id()?;
2453            Some(call_channel == channel_id)
2454        })
2455        .unwrap_or(false);
2456        let channel_store = self.channel_store.read(cx);
2457        let is_public = channel_store
2458            .channel_for_id(channel_id)
2459            .map(|channel| channel.visibility)
2460            == Some(proto::ChannelVisibility::Public);
2461        let disclosed =
2462            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2463
2464        let has_messages_notification = channel_store.has_new_messages(channel_id);
2465        let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
2466
2467        const FACEPILE_LIMIT: usize = 3;
2468        let participants = self.channel_store.read(cx).channel_participants(channel_id);
2469
2470        let face_pile = if !participants.is_empty() {
2471            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2472            let result = FacePile::new(
2473                participants
2474                    .iter()
2475                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
2476                    .take(FACEPILE_LIMIT)
2477                    .chain(if extra_count > 0 {
2478                        Some(
2479                            div()
2480                                .ml_2()
2481                                .child(Label::new(format!("+{extra_count}")))
2482                                .into_any_element(),
2483                        )
2484                    } else {
2485                        None
2486                    })
2487                    .collect::<SmallVec<_>>(),
2488            );
2489
2490            Some(result)
2491        } else {
2492            None
2493        };
2494
2495        let width = self.width.unwrap_or(px(240.));
2496        let root_id = channel.root_id();
2497
2498        div()
2499            .h_6()
2500            .id(channel_id as usize)
2501            .group("")
2502            .flex()
2503            .w_full()
2504            .when(!channel.is_root_channel(), |el| {
2505                el.on_drag(channel.clone(), move |channel, cx| {
2506                    cx.new_view(|_| DraggedChannelView {
2507                        channel: channel.clone(),
2508                        width,
2509                    })
2510                })
2511            })
2512            .drag_over::<Channel>({
2513                move |style, dragged_channel: &Channel, cx| {
2514                    if dragged_channel.root_id() == root_id {
2515                        style.bg(cx.theme().colors().ghost_element_hover)
2516                    } else {
2517                        style
2518                    }
2519                }
2520            })
2521            .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
2522                if dragged_channel.root_id() != root_id {
2523                    return;
2524                }
2525                this.move_channel(dragged_channel.id, channel_id, cx);
2526            }))
2527            .child(
2528                ListItem::new(channel_id as usize)
2529                    // Add one level of depth for the disclosure arrow.
2530                    .indent_level(depth + 1)
2531                    .indent_step_size(px(20.))
2532                    .selected(is_selected || is_active)
2533                    .toggle(disclosed)
2534                    .on_toggle(
2535                        cx.listener(move |this, _, cx| {
2536                            this.toggle_channel_collapsed(channel_id, cx)
2537                        }),
2538                    )
2539                    .on_click(cx.listener(move |this, _, cx| {
2540                        if is_active {
2541                            this.open_channel_notes(channel_id, cx)
2542                        } else {
2543                            this.join_channel(channel_id, cx)
2544                        }
2545                    }))
2546                    .on_secondary_mouse_down(cx.listener(
2547                        move |this, event: &MouseDownEvent, cx| {
2548                            this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2549                        },
2550                    ))
2551                    .start_slot(
2552                        div()
2553                            .relative()
2554                            .child(
2555                                Icon::new(if is_public {
2556                                    IconName::Public
2557                                } else {
2558                                    IconName::Hash
2559                                })
2560                                .size(IconSize::Small)
2561                                .color(Color::Muted),
2562                            )
2563                            .children(has_notes_notification.then(|| {
2564                                div()
2565                                    .w_1p5()
2566                                    .z_index(1)
2567                                    .absolute()
2568                                    .right(px(-1.))
2569                                    .top(px(-1.))
2570                                    .child(Indicator::dot().color(Color::Info))
2571                            })),
2572                    )
2573                    .child(
2574                        h_flex()
2575                            .id(channel_id as usize)
2576                            .child(Label::new(channel.name.clone()))
2577                            .children(face_pile.map(|face_pile| face_pile.p_1())),
2578                    ),
2579            )
2580            .child(
2581                h_flex()
2582                    .absolute()
2583                    .right(rems(0.))
2584                    .z_index(1)
2585                    .h_full()
2586                    .child(
2587                        h_flex()
2588                            .h_full()
2589                            .gap_1()
2590                            .px_1()
2591                            .child(
2592                                IconButton::new("channel_chat", IconName::MessageBubbles)
2593                                    .style(ButtonStyle::Filled)
2594                                    .shape(ui::IconButtonShape::Square)
2595                                    .icon_size(IconSize::Small)
2596                                    .icon_color(if has_messages_notification {
2597                                        Color::Default
2598                                    } else {
2599                                        Color::Muted
2600                                    })
2601                                    .on_click(cx.listener(move |this, _, cx| {
2602                                        this.join_channel_chat(channel_id, cx)
2603                                    }))
2604                                    .tooltip(|cx| Tooltip::text("Open channel chat", cx))
2605                                    .visible_on_hover(""),
2606                            )
2607                            .child(
2608                                IconButton::new("channel_notes", IconName::File)
2609                                    .style(ButtonStyle::Filled)
2610                                    .shape(ui::IconButtonShape::Square)
2611                                    .icon_size(IconSize::Small)
2612                                    .icon_color(if has_notes_notification {
2613                                        Color::Default
2614                                    } else {
2615                                        Color::Muted
2616                                    })
2617                                    .on_click(cx.listener(move |this, _, cx| {
2618                                        this.open_channel_notes(channel_id, cx)
2619                                    }))
2620                                    .tooltip(|cx| Tooltip::text("Open channel notes", cx))
2621                                    .visible_on_hover(""),
2622                            ),
2623                    ),
2624            )
2625            .tooltip({
2626                let channel_store = self.channel_store.clone();
2627                move |cx| {
2628                    cx.new_view(|_| JoinChannelTooltip {
2629                        channel_store: channel_store.clone(),
2630                        channel_id,
2631                        has_notes_notification,
2632                    })
2633                    .into()
2634                }
2635            })
2636    }
2637
2638    fn render_channel_editor(&self, depth: usize, _cx: &mut ViewContext<Self>) -> impl IntoElement {
2639        let item = ListItem::new("channel-editor")
2640            .inset(false)
2641            // Add one level of depth for the disclosure arrow.
2642            .indent_level(depth + 1)
2643            .indent_step_size(px(20.))
2644            .start_slot(
2645                Icon::new(IconName::Hash)
2646                    .size(IconSize::Small)
2647                    .color(Color::Muted),
2648            );
2649
2650        if let Some(pending_name) = self
2651            .channel_editing_state
2652            .as_ref()
2653            .and_then(|state| state.pending_name())
2654        {
2655            item.child(Label::new(pending_name))
2656        } else {
2657            item.child(self.channel_name_editor.clone())
2658        }
2659    }
2660}
2661
2662fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl IntoElement {
2663    let rem_size = cx.rem_size();
2664    let line_height = cx.text_style().line_height_in_pixels(rem_size);
2665    let width = rem_size * 1.5;
2666    let thickness = px(1.);
2667    let color = cx.theme().colors().text;
2668
2669    canvas(move |bounds, cx| {
2670        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2671        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2672        let right = bounds.right();
2673        let top = bounds.top();
2674
2675        cx.paint_quad(fill(
2676            Bounds::from_corners(
2677                point(start_x, top),
2678                point(
2679                    start_x + thickness,
2680                    if is_last {
2681                        start_y
2682                    } else {
2683                        bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
2684                    },
2685                ),
2686            ),
2687            color,
2688        ));
2689        cx.paint_quad(fill(
2690            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
2691            color,
2692        ));
2693    })
2694    .w(width)
2695    .h(line_height)
2696}
2697
2698impl Render for CollabPanel {
2699    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2700        v_flex()
2701            .key_context("CollabPanel")
2702            .on_action(cx.listener(CollabPanel::cancel))
2703            .on_action(cx.listener(CollabPanel::select_next))
2704            .on_action(cx.listener(CollabPanel::select_prev))
2705            .on_action(cx.listener(CollabPanel::confirm))
2706            .on_action(cx.listener(CollabPanel::insert_space))
2707            .on_action(cx.listener(CollabPanel::remove_selected_channel))
2708            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
2709            .on_action(cx.listener(CollabPanel::rename_selected_channel))
2710            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
2711            .on_action(cx.listener(CollabPanel::expand_selected_channel))
2712            .on_action(cx.listener(CollabPanel::start_move_selected_channel))
2713            .track_focus(&self.focus_handle)
2714            .size_full()
2715            .child(if self.user_store.read(cx).current_user().is_none() {
2716                self.render_signed_out(cx)
2717            } else {
2718                self.render_signed_in(cx)
2719            })
2720            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2721                overlay()
2722                    .position(*position)
2723                    .anchor(gpui::AnchorCorner::TopLeft)
2724                    .child(menu.clone())
2725            }))
2726    }
2727}
2728
2729impl EventEmitter<PanelEvent> for CollabPanel {}
2730
2731impl Panel for CollabPanel {
2732    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
2733        CollaborationPanelSettings::get_global(cx).dock
2734    }
2735
2736    fn position_is_valid(&self, position: DockPosition) -> bool {
2737        matches!(position, DockPosition::Left | DockPosition::Right)
2738    }
2739
2740    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2741        settings::update_settings_file::<CollaborationPanelSettings>(
2742            self.fs.clone(),
2743            cx,
2744            move |settings| settings.dock = Some(position),
2745        );
2746    }
2747
2748    fn size(&self, cx: &gpui::WindowContext) -> Pixels {
2749        self.width
2750            .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
2751    }
2752
2753    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
2754        self.width = size;
2755        self.serialize(cx);
2756        cx.notify();
2757    }
2758
2759    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
2760        CollaborationPanelSettings::get_global(cx)
2761            .button
2762            .then(|| ui::IconName::Collab)
2763    }
2764
2765    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
2766        Some("Collab Panel")
2767    }
2768
2769    fn toggle_action(&self) -> Box<dyn gpui::Action> {
2770        Box::new(ToggleFocus)
2771    }
2772
2773    fn persistent_name() -> &'static str {
2774        "CollabPanel"
2775    }
2776}
2777
2778impl FocusableView for CollabPanel {
2779    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
2780        self.filter_editor.focus_handle(cx).clone()
2781    }
2782}
2783
2784impl PartialEq for ListEntry {
2785    fn eq(&self, other: &Self) -> bool {
2786        match self {
2787            ListEntry::Header(section_1) => {
2788                if let ListEntry::Header(section_2) = other {
2789                    return section_1 == section_2;
2790                }
2791            }
2792            ListEntry::CallParticipant { user: user_1, .. } => {
2793                if let ListEntry::CallParticipant { user: user_2, .. } = other {
2794                    return user_1.id == user_2.id;
2795                }
2796            }
2797            ListEntry::ParticipantProject {
2798                project_id: project_id_1,
2799                ..
2800            } => {
2801                if let ListEntry::ParticipantProject {
2802                    project_id: project_id_2,
2803                    ..
2804                } = other
2805                {
2806                    return project_id_1 == project_id_2;
2807                }
2808            }
2809            ListEntry::ParticipantScreen {
2810                peer_id: peer_id_1, ..
2811            } => {
2812                if let ListEntry::ParticipantScreen {
2813                    peer_id: peer_id_2, ..
2814                } = other
2815                {
2816                    return peer_id_1 == peer_id_2;
2817                }
2818            }
2819            ListEntry::Channel {
2820                channel: channel_1, ..
2821            } => {
2822                if let ListEntry::Channel {
2823                    channel: channel_2, ..
2824                } = other
2825                {
2826                    return channel_1.id == channel_2.id;
2827                }
2828            }
2829            ListEntry::ChannelNotes { channel_id } => {
2830                if let ListEntry::ChannelNotes {
2831                    channel_id: other_id,
2832                } = other
2833                {
2834                    return channel_id == other_id;
2835                }
2836            }
2837            ListEntry::ChannelChat { channel_id } => {
2838                if let ListEntry::ChannelChat {
2839                    channel_id: other_id,
2840                } = other
2841                {
2842                    return channel_id == other_id;
2843                }
2844            }
2845            ListEntry::ChannelInvite(channel_1) => {
2846                if let ListEntry::ChannelInvite(channel_2) = other {
2847                    return channel_1.id == channel_2.id;
2848                }
2849            }
2850            ListEntry::IncomingRequest(user_1) => {
2851                if let ListEntry::IncomingRequest(user_2) = other {
2852                    return user_1.id == user_2.id;
2853                }
2854            }
2855            ListEntry::OutgoingRequest(user_1) => {
2856                if let ListEntry::OutgoingRequest(user_2) = other {
2857                    return user_1.id == user_2.id;
2858                }
2859            }
2860            ListEntry::Contact {
2861                contact: contact_1, ..
2862            } => {
2863                if let ListEntry::Contact {
2864                    contact: contact_2, ..
2865                } = other
2866                {
2867                    return contact_1.user.id == contact_2.user.id;
2868                }
2869            }
2870            ListEntry::ChannelEditor { depth } => {
2871                if let ListEntry::ChannelEditor { depth: other_depth } = other {
2872                    return depth == other_depth;
2873                }
2874            }
2875            ListEntry::ContactPlaceholder => {
2876                if let ListEntry::ContactPlaceholder = other {
2877                    return true;
2878                }
2879            }
2880        }
2881        false
2882    }
2883}
2884
2885struct DraggedChannelView {
2886    channel: Channel,
2887    width: Pixels,
2888}
2889
2890impl Render for DraggedChannelView {
2891    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
2892        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2893        h_flex()
2894            .font(ui_font)
2895            .bg(cx.theme().colors().background)
2896            .w(self.width)
2897            .p_1()
2898            .gap_1()
2899            .child(
2900                Icon::new(
2901                    if self.channel.visibility == proto::ChannelVisibility::Public {
2902                        IconName::Public
2903                    } else {
2904                        IconName::Hash
2905                    },
2906                )
2907                .size(IconSize::Small)
2908                .color(Color::Muted),
2909            )
2910            .child(Label::new(self.channel.name.clone()))
2911    }
2912}
2913
2914struct JoinChannelTooltip {
2915    channel_store: Model<ChannelStore>,
2916    channel_id: ChannelId,
2917    has_notes_notification: bool,
2918}
2919
2920impl Render for JoinChannelTooltip {
2921    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2922        tooltip_container(cx, |container, cx| {
2923            let participants = self
2924                .channel_store
2925                .read(cx)
2926                .channel_participants(self.channel_id);
2927
2928            container
2929                .child(Label::new("Join channel"))
2930                .children(self.has_notes_notification.then(|| {
2931                    h_flex()
2932                        .gap_2()
2933                        .child(Indicator::dot().color(Color::Info))
2934                        .child(Label::new("Unread notes"))
2935                }))
2936                .children(participants.iter().map(|participant| {
2937                    h_flex()
2938                        .gap_2()
2939                        .child(Avatar::new(participant.avatar_uri.clone()))
2940                        .child(Label::new(participant.github_login.clone()))
2941                }))
2942        })
2943    }
2944}