collab_panel.rs

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