collab_panel.rs

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