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