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, .. } => self.open_channel(channel.id, cx),
1483                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
1484                    ListEntry::CallParticipant { user, peer_id, .. } => {
1485                        if Some(user) == self.user_store.read(cx).current_user().as_ref() {
1486                            Self::leave_call(cx);
1487                        } else if let Some(peer_id) = peer_id {
1488                            self.workspace
1489                                .update(cx, |workspace, cx| workspace.follow(*peer_id, cx))
1490                                .ok();
1491                        }
1492                    }
1493                    ListEntry::IncomingRequest(user) => {
1494                        self.respond_to_contact_request(user.id, true, cx)
1495                    }
1496                    ListEntry::ChannelInvite(channel) => {
1497                        self.respond_to_channel_invite(channel.id, true, cx)
1498                    }
1499                    ListEntry::ChannelCall { channel_id } => {
1500                        self.join_channel_call(*channel_id, cx)
1501                    }
1502                    ListEntry::ChannelNotes { channel_id } => {
1503                        self.open_channel_notes(*channel_id, cx)
1504                    }
1505                    ListEntry::ChannelChat { channel_id } => {
1506                        self.join_channel_chat(*channel_id, cx)
1507                    }
1508
1509                    ListEntry::OutgoingRequest(_) => {}
1510                    ListEntry::ChannelEditor { .. } => {}
1511                }
1512            }
1513        }
1514    }
1515
1516    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
1517        if self.channel_editing_state.is_some() {
1518            self.channel_name_editor.update(cx, |editor, cx| {
1519                editor.insert(" ", cx);
1520            });
1521        }
1522    }
1523
1524    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
1525        if let Some(editing_state) = &mut self.channel_editing_state {
1526            match editing_state {
1527                ChannelEditingState::Create {
1528                    location,
1529                    pending_name,
1530                    ..
1531                } => {
1532                    if pending_name.is_some() {
1533                        return false;
1534                    }
1535                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1536
1537                    *pending_name = Some(channel_name.clone());
1538
1539                    self.channel_store
1540                        .update(cx, |channel_store, cx| {
1541                            channel_store.create_channel(&channel_name, *location, cx)
1542                        })
1543                        .detach();
1544                    cx.notify();
1545                }
1546                ChannelEditingState::Rename {
1547                    location,
1548                    pending_name,
1549                } => {
1550                    if pending_name.is_some() {
1551                        return false;
1552                    }
1553                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1554                    *pending_name = Some(channel_name.clone());
1555
1556                    self.channel_store
1557                        .update(cx, |channel_store, cx| {
1558                            channel_store.rename(*location, &channel_name, cx)
1559                        })
1560                        .detach();
1561                    cx.notify();
1562                }
1563            }
1564            cx.focus_self();
1565            true
1566        } else {
1567            false
1568        }
1569    }
1570
1571    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1572        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1573            self.collapsed_sections.remove(ix);
1574        } else {
1575            self.collapsed_sections.push(section);
1576        }
1577        self.update_entries(false, cx);
1578    }
1579
1580    fn collapse_selected_channel(
1581        &mut self,
1582        _: &CollapseSelectedChannel,
1583        cx: &mut ViewContext<Self>,
1584    ) {
1585        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1586            return;
1587        };
1588
1589        if self.is_channel_collapsed(channel_id) {
1590            return;
1591        }
1592
1593        self.toggle_channel_collapsed(channel_id, cx);
1594    }
1595
1596    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
1597        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1598            return;
1599        };
1600
1601        if !self.is_channel_collapsed(id) {
1602            return;
1603        }
1604
1605        self.toggle_channel_collapsed(id, cx)
1606    }
1607
1608    fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1609        match self.collapsed_channels.binary_search(&channel_id) {
1610            Ok(ix) => {
1611                self.collapsed_channels.remove(ix);
1612            }
1613            Err(ix) => {
1614                self.collapsed_channels.insert(ix, channel_id);
1615            }
1616        };
1617        self.serialize(cx);
1618        self.update_entries(true, cx);
1619        cx.notify();
1620        cx.focus_self();
1621    }
1622
1623    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1624        self.collapsed_channels.binary_search(&channel_id).is_ok()
1625    }
1626
1627    fn leave_call(cx: &mut WindowContext) {
1628        ActiveCall::global(cx)
1629            .update(cx, |call, cx| call.hang_up(cx))
1630            .detach_and_prompt_err("Failed to hang up", cx, |_, _| None);
1631    }
1632
1633    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1634        if let Some(workspace) = self.workspace.upgrade() {
1635            workspace.update(cx, |workspace, cx| {
1636                workspace.toggle_modal(cx, |cx| {
1637                    let mut finder = ContactFinder::new(self.user_store.clone(), cx);
1638                    finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1639                    finder
1640                });
1641            });
1642        }
1643    }
1644
1645    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
1646        self.channel_editing_state = Some(ChannelEditingState::Create {
1647            location: None,
1648            pending_name: None,
1649        });
1650        self.update_entries(false, cx);
1651        self.select_channel_editor();
1652        cx.focus_view(&self.channel_name_editor);
1653        cx.notify();
1654    }
1655
1656    fn select_channel_editor(&mut self) {
1657        self.selection = self.entries.iter().position(|entry| match entry {
1658            ListEntry::ChannelEditor { .. } => true,
1659            _ => false,
1660        });
1661    }
1662
1663    fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1664        self.collapsed_channels
1665            .retain(|channel| *channel != channel_id);
1666        self.channel_editing_state = Some(ChannelEditingState::Create {
1667            location: Some(channel_id),
1668            pending_name: None,
1669        });
1670        self.update_entries(false, cx);
1671        self.select_channel_editor();
1672        cx.focus_view(&self.channel_name_editor);
1673        cx.notify();
1674    }
1675
1676    fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1677        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
1678    }
1679
1680    fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
1681        if let Some(channel) = self.selected_channel() {
1682            self.remove_channel(channel.id, cx)
1683        }
1684    }
1685
1686    fn rename_selected_channel(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext<Self>) {
1687        if let Some(channel) = self.selected_channel() {
1688            self.rename_channel(channel.id, cx);
1689        }
1690    }
1691
1692    fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1693        let channel_store = self.channel_store.read(cx);
1694        if !channel_store.is_channel_admin(channel_id) {
1695            return;
1696        }
1697        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
1698            self.channel_editing_state = Some(ChannelEditingState::Rename {
1699                location: channel_id,
1700                pending_name: None,
1701            });
1702            self.channel_name_editor.update(cx, |editor, cx| {
1703                editor.set_text(channel.name.clone(), cx);
1704                editor.select_all(&Default::default(), cx);
1705            });
1706            cx.focus_view(&self.channel_name_editor);
1707            self.update_entries(false, cx);
1708            self.select_channel_editor();
1709        }
1710    }
1711
1712    fn set_channel_visibility(
1713        &mut self,
1714        channel_id: ChannelId,
1715        visibility: ChannelVisibility,
1716        cx: &mut ViewContext<Self>,
1717    ) {
1718        self.channel_store
1719            .update(cx, |channel_store, cx| {
1720                channel_store.set_channel_visibility(channel_id, visibility, cx)
1721            })
1722            .detach_and_prompt_err("Failed to set channel visibility", cx, |e, _| match e.error_code() {
1723                ErrorCode::BadPublicNesting =>
1724                    if e.error_tag("direction") == Some("parent") {
1725                        Some("To make a channel public, its parent channel must be public.".to_string())
1726                    } else {
1727                        Some("To make a channel private, all of its subchannels must be private.".to_string())
1728                    },
1729                _ => None
1730            });
1731    }
1732
1733    fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
1734        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
1735    }
1736
1737    fn start_move_selected_channel(&mut self, _: &StartMoveChannel, cx: &mut ViewContext<Self>) {
1738        if let Some(channel) = self.selected_channel() {
1739            self.start_move_channel(channel.id, cx);
1740        }
1741    }
1742
1743    fn move_channel_on_clipboard(
1744        &mut self,
1745        to_channel_id: ChannelId,
1746        cx: &mut ViewContext<CollabPanel>,
1747    ) {
1748        if let Some(clipboard) = self.channel_clipboard.take() {
1749            self.move_channel(clipboard.channel_id, to_channel_id, cx)
1750        }
1751    }
1752
1753    fn move_channel(&self, channel_id: ChannelId, to: ChannelId, cx: &mut ViewContext<Self>) {
1754        self.channel_store
1755            .update(cx, |channel_store, cx| {
1756                channel_store.move_channel(channel_id, to, cx)
1757            })
1758            .detach_and_prompt_err("Failed to move channel", cx, |e, _| match e.error_code() {
1759                ErrorCode::BadPublicNesting => {
1760                    Some("Public channels must have public parents".into())
1761                }
1762                ErrorCode::CircularNesting => Some("You cannot move a channel into itself".into()),
1763                ErrorCode::WrongMoveTarget => {
1764                    Some("You cannot move a channel into a different root channel".into())
1765                }
1766                _ => None,
1767            })
1768    }
1769
1770    fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1771        if let Some(workspace) = self.workspace.upgrade() {
1772            ChannelView::open(channel_id, None, workspace, cx).detach();
1773        }
1774    }
1775
1776    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
1777        let Some(bounds) = self
1778            .selection
1779            .and_then(|ix| self.list_state.bounds_for_item(ix))
1780        else {
1781            return;
1782        };
1783
1784        if let Some(channel) = self.selected_channel() {
1785            self.deploy_channel_context_menu(
1786                bounds.center(),
1787                channel.id,
1788                self.selection.unwrap(),
1789                cx,
1790            );
1791            cx.stop_propagation();
1792            return;
1793        };
1794
1795        if let Some(contact) = self.selected_contact() {
1796            self.deploy_contact_context_menu(bounds.center(), contact, cx);
1797            cx.stop_propagation();
1798            return;
1799        };
1800    }
1801
1802    fn selected_channel(&self) -> Option<&Arc<Channel>> {
1803        self.selection
1804            .and_then(|ix| self.entries.get(ix))
1805            .and_then(|entry| match entry {
1806                ListEntry::Channel { channel, .. } => Some(channel),
1807                _ => None,
1808            })
1809    }
1810
1811    fn selected_contact(&self) -> Option<Arc<Contact>> {
1812        self.selection
1813            .and_then(|ix| self.entries.get(ix))
1814            .and_then(|entry| match entry {
1815                ListEntry::Contact { contact, .. } => Some(contact.clone()),
1816                _ => None,
1817            })
1818    }
1819
1820    fn show_channel_modal(
1821        &mut self,
1822        channel_id: ChannelId,
1823        mode: channel_modal::Mode,
1824        cx: &mut ViewContext<Self>,
1825    ) {
1826        let workspace = self.workspace.clone();
1827        let user_store = self.user_store.clone();
1828        let channel_store = self.channel_store.clone();
1829        let members = self.channel_store.update(cx, |channel_store, cx| {
1830            channel_store.get_channel_member_details(channel_id, cx)
1831        });
1832
1833        cx.spawn(|_, mut cx| async move {
1834            let members = members.await?;
1835            workspace.update(&mut cx, |workspace, cx| {
1836                workspace.toggle_modal(cx, |cx| {
1837                    ChannelModal::new(
1838                        user_store.clone(),
1839                        channel_store.clone(),
1840                        channel_id,
1841                        mode,
1842                        members,
1843                        cx,
1844                    )
1845                });
1846            })
1847        })
1848        .detach();
1849    }
1850
1851    fn leave_channel(&self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1852        let Some(user_id) = self.user_store.read(cx).current_user().map(|u| u.id) else {
1853            return;
1854        };
1855        let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) else {
1856            return;
1857        };
1858        let prompt_message = format!("Are you sure you want to leave \"#{}\"?", channel.name);
1859        let answer = cx.prompt(
1860            PromptLevel::Warning,
1861            &prompt_message,
1862            None,
1863            &["Leave", "Cancel"],
1864        );
1865        cx.spawn(|this, mut cx| async move {
1866            if answer.await? != 0 {
1867                return Ok(());
1868            }
1869            this.update(&mut cx, |this, cx| {
1870                this.channel_store.update(cx, |channel_store, cx| {
1871                    channel_store.remove_member(channel_id, user_id, cx)
1872                })
1873            })?
1874            .await
1875        })
1876        .detach_and_prompt_err("Failed to leave channel", cx, |_, _| None)
1877    }
1878
1879    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1880        let channel_store = self.channel_store.clone();
1881        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
1882            let prompt_message = format!(
1883                "Are you sure you want to remove the channel \"{}\"?",
1884                channel.name
1885            );
1886            let answer = cx.prompt(
1887                PromptLevel::Warning,
1888                &prompt_message,
1889                None,
1890                &["Remove", "Cancel"],
1891            );
1892            cx.spawn(|this, mut cx| async move {
1893                if answer.await? == 0 {
1894                    channel_store
1895                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
1896                        .await
1897                        .notify_async_err(&mut cx);
1898                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
1899                }
1900                anyhow::Ok(())
1901            })
1902            .detach();
1903        }
1904    }
1905
1906    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
1907        let user_store = self.user_store.clone();
1908        let prompt_message = format!(
1909            "Are you sure you want to remove \"{}\" from your contacts?",
1910            github_login
1911        );
1912        let answer = cx.prompt(
1913            PromptLevel::Warning,
1914            &prompt_message,
1915            None,
1916            &["Remove", "Cancel"],
1917        );
1918        cx.spawn(|_, mut cx| async move {
1919            if answer.await? == 0 {
1920                user_store
1921                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
1922                    .await
1923                    .notify_async_err(&mut cx);
1924            }
1925            anyhow::Ok(())
1926        })
1927        .detach_and_prompt_err("Failed to remove contact", cx, |_, _| None);
1928    }
1929
1930    fn respond_to_contact_request(
1931        &mut self,
1932        user_id: u64,
1933        accept: bool,
1934        cx: &mut ViewContext<Self>,
1935    ) {
1936        self.user_store
1937            .update(cx, |store, cx| {
1938                store.respond_to_contact_request(user_id, accept, cx)
1939            })
1940            .detach_and_prompt_err("Failed to respond to contact request", cx, |_, _| None);
1941    }
1942
1943    fn respond_to_channel_invite(
1944        &mut self,
1945        channel_id: u64,
1946        accept: bool,
1947        cx: &mut ViewContext<Self>,
1948    ) {
1949        self.channel_store
1950            .update(cx, |store, cx| {
1951                store.respond_to_channel_invite(channel_id, accept, cx)
1952            })
1953            .detach();
1954    }
1955
1956    fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext<Self>) {
1957        ActiveCall::global(cx)
1958            .update(cx, |call, cx| {
1959                call.invite(recipient_user_id, Some(self.project.clone()), cx)
1960            })
1961            .detach_and_prompt_err("Call failed", cx, |_, _| None);
1962    }
1963
1964    fn open_channel(&mut self, channel_id: u64, cx: &mut ViewContext<Self>) {
1965        let Some(workspace) = self.workspace.upgrade() else {
1966            return;
1967        };
1968        let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
1969            return;
1970        };
1971        let is_in_call = ActiveCall::global(cx)
1972            .read(cx)
1973            .room()
1974            .map(|room| room.read(cx).in_call())
1975            .unwrap_or(false);
1976        if !is_in_call {
1977            workspace::open_channel(
1978                channel_id,
1979                workspace.read(cx).app_state().clone(),
1980                Some(handle),
1981                cx,
1982            )
1983            .detach_and_prompt_err("Failed to join channel", cx, |_, _| None);
1984        }
1985
1986        self.open_channel_notes(channel_id, cx);
1987        self.join_channel_chat(channel_id, cx);
1988    }
1989
1990    fn join_channel_call(&mut self, _channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1991        let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
1992            return;
1993        };
1994
1995        room.update(cx, |room, cx| room.join_call(cx))
1996            .detach_and_prompt_err("Failed to join call", cx, |_, _| None)
1997    }
1998
1999    fn leave_channel_call(&mut self, cx: &mut ViewContext<Self>) {
2000        let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
2001            return;
2002        };
2003
2004        room.update(cx, |room, cx| room.leave_call(cx));
2005    }
2006
2007    fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2008        let Some(workspace) = self.workspace.upgrade() else {
2009            return;
2010        };
2011        cx.window_context().defer(move |cx| {
2012            workspace.update(cx, |workspace, cx| {
2013                if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
2014                    panel.update(cx, |panel, cx| {
2015                        panel
2016                            .select_channel(channel_id, None, cx)
2017                            .detach_and_notify_err(cx);
2018                    });
2019                }
2020            });
2021        });
2022    }
2023
2024    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2025        let channel_store = self.channel_store.read(cx);
2026        let Some(channel) = channel_store.channel_for_id(channel_id) else {
2027            return;
2028        };
2029        let item = ClipboardItem::new(channel.link());
2030        cx.write_to_clipboard(item)
2031    }
2032
2033    fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
2034        let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
2035
2036        v_flex()
2037            .gap_6()
2038            .p_4()
2039            .child(Label::new(collab_blurb))
2040            .child(
2041                v_flex()
2042                    .gap_2()
2043                    .child(
2044                        Button::new("sign_in", "Sign in")
2045                            .icon_color(Color::Muted)
2046                            .icon(IconName::Github)
2047                            .icon_position(IconPosition::Start)
2048                            .style(ButtonStyle::Filled)
2049                            .full_width()
2050                            .on_click(cx.listener(|this, _, cx| {
2051                                let client = this.client.clone();
2052                                cx.spawn(|_, mut cx| async move {
2053                                    client
2054                                        .authenticate_and_connect(true, &cx)
2055                                        .await
2056                                        .notify_async_err(&mut cx);
2057                                })
2058                                .detach()
2059                            })),
2060                    )
2061                    .child(
2062                        div().flex().w_full().items_center().child(
2063                            Label::new("Sign in to enable collaboration.")
2064                                .color(Color::Muted)
2065                                .size(LabelSize::Small),
2066                        ),
2067                    ),
2068            )
2069    }
2070
2071    fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
2072        let entry = &self.entries[ix];
2073
2074        let is_selected = self.selection == Some(ix);
2075        match entry {
2076            ListEntry::Header(section) => {
2077                let is_collapsed = self.collapsed_sections.contains(section);
2078                self.render_header(*section, is_selected, is_collapsed, cx)
2079                    .into_any_element()
2080            }
2081            ListEntry::Contact { contact, calling } => self
2082                .render_contact(contact, *calling, is_selected, cx)
2083                .into_any_element(),
2084            ListEntry::ContactPlaceholder => self
2085                .render_contact_placeholder(is_selected, cx)
2086                .into_any_element(),
2087            ListEntry::IncomingRequest(user) => self
2088                .render_contact_request(user, true, is_selected, cx)
2089                .into_any_element(),
2090            ListEntry::OutgoingRequest(user) => self
2091                .render_contact_request(user, false, is_selected, cx)
2092                .into_any_element(),
2093            ListEntry::Channel {
2094                channel,
2095                depth,
2096                has_children,
2097            } => self
2098                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
2099                .into_any_element(),
2100            ListEntry::ChannelEditor { depth } => {
2101                self.render_channel_editor(*depth, cx).into_any_element()
2102            }
2103            ListEntry::ChannelInvite(channel) => self
2104                .render_channel_invite(channel, is_selected, cx)
2105                .into_any_element(),
2106            ListEntry::CallParticipant {
2107                user,
2108                peer_id,
2109                is_pending,
2110                role,
2111            } => self
2112                .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
2113                .into_any_element(),
2114            ListEntry::ParticipantProject {
2115                project_id,
2116                worktree_root_names,
2117                host_user_id,
2118                is_last,
2119            } => self
2120                .render_participant_project(
2121                    *project_id,
2122                    &worktree_root_names,
2123                    *host_user_id,
2124                    *is_last,
2125                    is_selected,
2126                    cx,
2127                )
2128                .into_any_element(),
2129            ListEntry::ParticipantScreen { peer_id, is_last } => self
2130                .render_participant_screen(*peer_id, *is_last, is_selected, cx)
2131                .into_any_element(),
2132            ListEntry::ChannelCall { channel_id } => self
2133                .render_channel_call(*channel_id, is_selected, cx)
2134                .into_any_element(),
2135            ListEntry::ChannelNotes { channel_id } => self
2136                .render_channel_notes(*channel_id, is_selected, cx)
2137                .into_any_element(),
2138            ListEntry::ChannelChat { channel_id } => self
2139                .render_channel_chat(*channel_id, is_selected, cx)
2140                .into_any_element(),
2141        }
2142    }
2143
2144    fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
2145        v_flex()
2146            .size_full()
2147            .child(list(self.list_state.clone()).full())
2148            .child(
2149                v_flex()
2150                    .child(div().mx_2().border_primary(cx).border_t())
2151                    .child(
2152                        v_flex()
2153                            .p_2()
2154                            .child(self.render_filter_input(&self.filter_editor, cx)),
2155                    ),
2156            )
2157    }
2158
2159    fn render_filter_input(
2160        &self,
2161        editor: &View<Editor>,
2162        cx: &mut ViewContext<Self>,
2163    ) -> impl IntoElement {
2164        let settings = ThemeSettings::get_global(cx);
2165        let text_style = TextStyle {
2166            color: if editor.read(cx).read_only(cx) {
2167                cx.theme().colors().text_disabled
2168            } else {
2169                cx.theme().colors().text
2170            },
2171            font_family: settings.ui_font.family.clone(),
2172            font_features: settings.ui_font.features,
2173            font_size: rems(0.875).into(),
2174            font_weight: FontWeight::NORMAL,
2175            font_style: FontStyle::Normal,
2176            line_height: relative(1.3).into(),
2177            background_color: None,
2178            underline: None,
2179            strikethrough: None,
2180            white_space: WhiteSpace::Normal,
2181        };
2182
2183        EditorElement::new(
2184            editor,
2185            EditorStyle {
2186                local_player: cx.theme().players().local(),
2187                text: text_style,
2188                ..Default::default()
2189            },
2190        )
2191    }
2192
2193    fn render_header(
2194        &self,
2195        section: Section,
2196        is_selected: bool,
2197        is_collapsed: bool,
2198        cx: &ViewContext<Self>,
2199    ) -> impl IntoElement {
2200        let mut channel_tooltip_text = None;
2201        let mut channel_icon = None;
2202
2203        let text = match section {
2204            Section::ActiveCall => {
2205                let channel_name = maybe!({
2206                    let channel_id = ActiveCall::global(cx)
2207                        .read(cx)
2208                        .channel_id(cx)
2209                        .or_else(|| ActiveCall::global(cx).read(cx).pending_channel_id())?;
2210
2211                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2212
2213                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2214                        proto::ChannelVisibility::Public => {
2215                            (Some(IconName::Public), Some("Close Channel"))
2216                        }
2217                        proto::ChannelVisibility::Members => {
2218                            (Some(IconName::Hash), Some("Close Channel"))
2219                        }
2220                    };
2221
2222                    Some(channel.name.as_ref())
2223                });
2224
2225                if let Some(name) = channel_name {
2226                    SharedString::from(format!("{}", name))
2227                } else {
2228                    SharedString::from("Current Call")
2229                }
2230            }
2231            Section::ContactRequests => SharedString::from("Requests"),
2232            Section::Contacts => SharedString::from("Contacts"),
2233            Section::Channels => SharedString::from("Channels"),
2234            Section::ChannelInvites => SharedString::from("Invites"),
2235            Section::Online => SharedString::from("Online"),
2236            Section::Offline => SharedString::from("Offline"),
2237        };
2238
2239        let button = match section {
2240            Section::ActiveCall => channel_icon.map(|_| {
2241                IconButton::new("channel-link", IconName::Close)
2242                    .on_click(move |_, cx| Self::leave_call(cx))
2243                    .tooltip(|cx| Tooltip::text("Close channel", cx))
2244                    .into_any_element()
2245            }),
2246            Section::Contacts => Some(
2247                IconButton::new("add-contact", IconName::Plus)
2248                    .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2249                    .tooltip(|cx| Tooltip::text("Search for new contact", cx))
2250                    .into_any_element(),
2251            ),
2252            Section::Channels => Some(
2253                IconButton::new("add-channel", IconName::Plus)
2254                    .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
2255                    .tooltip(|cx| Tooltip::text("Create a channel", cx))
2256                    .into_any_element(),
2257            ),
2258            _ => None,
2259        };
2260
2261        let can_collapse = match section {
2262            Section::ActiveCall | Section::Channels | Section::Contacts => false,
2263            Section::ChannelInvites
2264            | Section::ContactRequests
2265            | Section::Online
2266            | Section::Offline => true,
2267        };
2268
2269        h_flex().w_full().group("section-header").child(
2270            ListHeader::new(text)
2271                .when(can_collapse, |header| {
2272                    header
2273                        .toggle(Some(!is_collapsed))
2274                        .on_toggle(cx.listener(move |this, _, cx| {
2275                            this.toggle_section_expanded(section, cx);
2276                        }))
2277                })
2278                .when_some(channel_icon, |el, channel_icon| {
2279                    el.start_slot(Icon::new(channel_icon).color(Color::Muted))
2280                })
2281                .inset(true)
2282                .end_slot::<AnyElement>(button)
2283                .selected(is_selected),
2284        )
2285    }
2286
2287    fn render_contact(
2288        &self,
2289        contact: &Arc<Contact>,
2290        calling: bool,
2291        is_selected: bool,
2292        cx: &mut ViewContext<Self>,
2293    ) -> impl IntoElement {
2294        let online = contact.online;
2295        let busy = contact.busy || calling;
2296        let github_login = SharedString::from(contact.user.github_login.clone());
2297        let item = ListItem::new(github_login.clone())
2298            .indent_level(1)
2299            .indent_step_size(px(20.))
2300            .selected(is_selected)
2301            .child(
2302                h_flex()
2303                    .w_full()
2304                    .justify_between()
2305                    .child(Label::new(github_login.clone()))
2306                    .when(calling, |el| {
2307                        el.child(Label::new("Calling").color(Color::Muted))
2308                    })
2309                    .when(!calling, |el| {
2310                        el.child(
2311                            IconButton::new("contact context menu", IconName::Ellipsis)
2312                                .icon_color(Color::Muted)
2313                                .visible_on_hover("")
2314                                .on_click(cx.listener({
2315                                    let contact = contact.clone();
2316                                    move |this, event: &ClickEvent, cx| {
2317                                        this.deploy_contact_context_menu(
2318                                            event.down.position,
2319                                            contact.clone(),
2320                                            cx,
2321                                        );
2322                                    }
2323                                })),
2324                        )
2325                    }),
2326            )
2327            .on_secondary_mouse_down(cx.listener({
2328                let contact = contact.clone();
2329                move |this, event: &MouseDownEvent, cx| {
2330                    this.deploy_contact_context_menu(event.position, contact.clone(), cx);
2331                }
2332            }))
2333            .start_slot(
2334                // todo handle contacts with no avatar
2335                Avatar::new(contact.user.avatar_uri.clone())
2336                    .indicator::<AvatarAvailabilityIndicator>(if online {
2337                        Some(AvatarAvailabilityIndicator::new(match busy {
2338                            true => ui::Availability::Busy,
2339                            false => ui::Availability::Free,
2340                        }))
2341                    } else {
2342                        None
2343                    }),
2344            );
2345
2346        div()
2347            .id(github_login.clone())
2348            .group("")
2349            .child(item)
2350            .tooltip(move |cx| {
2351                let text = if !online {
2352                    format!(" {} is offline", &github_login)
2353                } else if busy {
2354                    format!(" {} is on a call", &github_login)
2355                } else {
2356                    let room = ActiveCall::global(cx).read(cx).room();
2357                    if room.is_some() {
2358                        format!("Invite {} to join call", &github_login)
2359                    } else {
2360                        format!("Call {}", &github_login)
2361                    }
2362                };
2363                Tooltip::text(text, cx)
2364            })
2365    }
2366
2367    fn render_contact_request(
2368        &self,
2369        user: &Arc<User>,
2370        is_incoming: bool,
2371        is_selected: bool,
2372        cx: &mut ViewContext<Self>,
2373    ) -> impl IntoElement {
2374        let github_login = SharedString::from(user.github_login.clone());
2375        let user_id = user.id;
2376        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2377        let color = if is_response_pending {
2378            Color::Muted
2379        } else {
2380            Color::Default
2381        };
2382
2383        let controls = if is_incoming {
2384            vec![
2385                IconButton::new("decline-contact", IconName::Close)
2386                    .on_click(cx.listener(move |this, _, cx| {
2387                        this.respond_to_contact_request(user_id, false, cx);
2388                    }))
2389                    .icon_color(color)
2390                    .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2391                IconButton::new("accept-contact", IconName::Check)
2392                    .on_click(cx.listener(move |this, _, cx| {
2393                        this.respond_to_contact_request(user_id, true, cx);
2394                    }))
2395                    .icon_color(color)
2396                    .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2397            ]
2398        } else {
2399            let github_login = github_login.clone();
2400            vec![IconButton::new("remove_contact", IconName::Close)
2401                .on_click(cx.listener(move |this, _, cx| {
2402                    this.remove_contact(user_id, &github_login, cx);
2403                }))
2404                .icon_color(color)
2405                .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2406        };
2407
2408        ListItem::new(github_login.clone())
2409            .indent_level(1)
2410            .indent_step_size(px(20.))
2411            .selected(is_selected)
2412            .child(
2413                h_flex()
2414                    .w_full()
2415                    .justify_between()
2416                    .child(Label::new(github_login.clone()))
2417                    .child(h_flex().children(controls)),
2418            )
2419            .start_slot(Avatar::new(user.avatar_uri.clone()))
2420    }
2421
2422    fn render_channel_invite(
2423        &self,
2424        channel: &Arc<Channel>,
2425        is_selected: bool,
2426        cx: &mut ViewContext<Self>,
2427    ) -> ListItem {
2428        let channel_id = channel.id;
2429        let response_is_pending = self
2430            .channel_store
2431            .read(cx)
2432            .has_pending_channel_invite_response(&channel);
2433        let color = if response_is_pending {
2434            Color::Muted
2435        } else {
2436            Color::Default
2437        };
2438
2439        let controls = [
2440            IconButton::new("reject-invite", IconName::Close)
2441                .on_click(cx.listener(move |this, _, cx| {
2442                    this.respond_to_channel_invite(channel_id, false, cx);
2443                }))
2444                .icon_color(color)
2445                .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2446            IconButton::new("accept-invite", IconName::Check)
2447                .on_click(cx.listener(move |this, _, cx| {
2448                    this.respond_to_channel_invite(channel_id, true, cx);
2449                }))
2450                .icon_color(color)
2451                .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2452        ];
2453
2454        ListItem::new(("channel-invite", channel.id as usize))
2455            .selected(is_selected)
2456            .child(
2457                h_flex()
2458                    .w_full()
2459                    .justify_between()
2460                    .child(Label::new(channel.name.clone()))
2461                    .child(h_flex().children(controls)),
2462            )
2463            .start_slot(
2464                Icon::new(IconName::Hash)
2465                    .size(IconSize::Small)
2466                    .color(Color::Muted),
2467            )
2468    }
2469
2470    fn render_contact_placeholder(
2471        &self,
2472        is_selected: bool,
2473        cx: &mut ViewContext<Self>,
2474    ) -> ListItem {
2475        ListItem::new("contact-placeholder")
2476            .child(Icon::new(IconName::Plus))
2477            .child(Label::new("Add a Contact"))
2478            .selected(is_selected)
2479            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2480    }
2481
2482    fn render_channel(
2483        &self,
2484        channel: &Channel,
2485        depth: usize,
2486        has_children: bool,
2487        is_selected: bool,
2488        ix: usize,
2489        cx: &mut ViewContext<Self>,
2490    ) -> impl IntoElement {
2491        let channel_id = channel.id;
2492
2493        let is_active = maybe!({
2494            let call_channel = ActiveCall::global(cx)
2495                .read(cx)
2496                .room()?
2497                .read(cx)
2498                .channel_id()?;
2499            Some(call_channel == channel_id)
2500        })
2501        .unwrap_or(false);
2502        let channel_store = self.channel_store.read(cx);
2503        let is_public = channel_store
2504            .channel_for_id(channel_id)
2505            .map(|channel| channel.visibility)
2506            == Some(proto::ChannelVisibility::Public);
2507        let disclosed =
2508            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2509
2510        let has_messages_notification = channel_store.has_new_messages(channel_id);
2511        let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
2512
2513        const FACEPILE_LIMIT: usize = 3;
2514        let participants = self.channel_store.read(cx).channel_participants(channel_id);
2515
2516        let face_pile = if !participants.is_empty() {
2517            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2518            let result = FacePile::new(
2519                participants
2520                    .iter()
2521                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
2522                    .take(FACEPILE_LIMIT)
2523                    .chain(if extra_count > 0 {
2524                        Some(
2525                            div()
2526                                .ml_2()
2527                                .child(Label::new(format!("+{extra_count}")))
2528                                .into_any_element(),
2529                        )
2530                    } else {
2531                        None
2532                    })
2533                    .collect::<SmallVec<_>>(),
2534            );
2535
2536            Some(result)
2537        } else {
2538            None
2539        };
2540
2541        let width = self.width.unwrap_or(px(240.));
2542        let root_id = channel.root_id();
2543
2544        div()
2545            .h_6()
2546            .id(channel_id as usize)
2547            .group("")
2548            .flex()
2549            .w_full()
2550            .when(!channel.is_root_channel(), |el| {
2551                el.on_drag(channel.clone(), move |channel, cx| {
2552                    cx.new_view(|_| DraggedChannelView {
2553                        channel: channel.clone(),
2554                        width,
2555                    })
2556                })
2557            })
2558            .drag_over::<Channel>({
2559                move |style, dragged_channel: &Channel, cx| {
2560                    if dragged_channel.root_id() == root_id {
2561                        style.bg(cx.theme().colors().ghost_element_hover)
2562                    } else {
2563                        style
2564                    }
2565                }
2566            })
2567            .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
2568                if dragged_channel.root_id() != root_id {
2569                    return;
2570                }
2571                this.move_channel(dragged_channel.id, channel_id, cx);
2572            }))
2573            .child(
2574                ListItem::new(channel_id as usize)
2575                    // Add one level of depth for the disclosure arrow.
2576                    .indent_level(depth + 1)
2577                    .indent_step_size(px(20.))
2578                    .selected(is_selected || is_active)
2579                    .toggle(disclosed)
2580                    .on_toggle(
2581                        cx.listener(move |this, _, cx| {
2582                            this.toggle_channel_collapsed(channel_id, cx)
2583                        }),
2584                    )
2585                    .on_click(cx.listener(move |this, _, cx| {
2586                        this.open_channel(channel_id, cx);
2587                    }))
2588                    .on_secondary_mouse_down(cx.listener(
2589                        move |this, event: &MouseDownEvent, cx| {
2590                            this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2591                        },
2592                    ))
2593                    .start_slot(
2594                        Icon::new(if is_public {
2595                            IconName::Public
2596                        } else {
2597                            IconName::Hash
2598                        })
2599                        .size(IconSize::Small)
2600                        .color(Color::Muted),
2601                    )
2602                    .child(
2603                        h_flex().id(channel_id as usize).child(
2604                            div()
2605                                .text_ui()
2606                                .when(has_messages_notification || has_notes_notification, |el| {
2607                                    el.font_weight(FontWeight::SEMIBOLD)
2608                                })
2609                                .child(channel.name.clone()),
2610                        ),
2611                    ),
2612            )
2613            .children(face_pile.map(|face_pile| {
2614                h_flex()
2615                    .absolute()
2616                    .right(rems(0.))
2617                    .z_index(1)
2618                    .h_full()
2619                    .child(face_pile.p_1())
2620            }))
2621            .tooltip({
2622                let channel_store = self.channel_store.clone();
2623                move |cx| {
2624                    cx.new_view(|_| JoinChannelTooltip {
2625                        channel_store: channel_store.clone(),
2626                        channel_id,
2627                    })
2628                    .into()
2629                }
2630            })
2631    }
2632
2633    fn render_channel_editor(&self, depth: usize, _cx: &mut ViewContext<Self>) -> impl IntoElement {
2634        let item = ListItem::new("channel-editor")
2635            .inset(false)
2636            // Add one level of depth for the disclosure arrow.
2637            .indent_level(depth + 1)
2638            .indent_step_size(px(20.))
2639            .start_slot(
2640                Icon::new(IconName::Hash)
2641                    .size(IconSize::Small)
2642                    .color(Color::Muted),
2643            );
2644
2645        if let Some(pending_name) = self
2646            .channel_editing_state
2647            .as_ref()
2648            .and_then(|state| state.pending_name())
2649        {
2650            item.child(Label::new(pending_name))
2651        } else {
2652            item.child(self.channel_name_editor.clone())
2653        }
2654    }
2655}
2656
2657fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl IntoElement {
2658    let rem_size = cx.rem_size();
2659    let line_height = cx.text_style().line_height_in_pixels(rem_size);
2660    let width = rem_size * 1.5;
2661    let thickness = px(1.);
2662    let color = cx.theme().colors().text;
2663
2664    canvas(move |bounds, cx| {
2665        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2666        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2667        let right = bounds.right();
2668        let top = bounds.top();
2669
2670        cx.paint_quad(fill(
2671            Bounds::from_corners(
2672                point(start_x, top),
2673                point(
2674                    start_x + thickness,
2675                    if is_last {
2676                        start_y
2677                    } else {
2678                        bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
2679                    },
2680                ),
2681            ),
2682            color,
2683        ));
2684        cx.paint_quad(fill(
2685            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
2686            color,
2687        ));
2688    })
2689    .w(width)
2690    .h(line_height)
2691}
2692
2693impl Render for CollabPanel {
2694    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2695        v_flex()
2696            .key_context("CollabPanel")
2697            .on_action(cx.listener(CollabPanel::cancel))
2698            .on_action(cx.listener(CollabPanel::select_next))
2699            .on_action(cx.listener(CollabPanel::select_prev))
2700            .on_action(cx.listener(CollabPanel::confirm))
2701            .on_action(cx.listener(CollabPanel::insert_space))
2702            .on_action(cx.listener(CollabPanel::remove_selected_channel))
2703            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
2704            .on_action(cx.listener(CollabPanel::rename_selected_channel))
2705            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
2706            .on_action(cx.listener(CollabPanel::expand_selected_channel))
2707            .on_action(cx.listener(CollabPanel::start_move_selected_channel))
2708            .track_focus(&self.focus_handle)
2709            .size_full()
2710            .child(if self.user_store.read(cx).current_user().is_none() {
2711                self.render_signed_out(cx)
2712            } else {
2713                self.render_signed_in(cx)
2714            })
2715            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2716                overlay()
2717                    .position(*position)
2718                    .anchor(gpui::AnchorCorner::TopLeft)
2719                    .child(menu.clone())
2720            }))
2721    }
2722}
2723
2724impl EventEmitter<PanelEvent> for CollabPanel {}
2725
2726impl Panel for CollabPanel {
2727    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
2728        CollaborationPanelSettings::get_global(cx).dock
2729    }
2730
2731    fn position_is_valid(&self, position: DockPosition) -> bool {
2732        matches!(position, DockPosition::Left | DockPosition::Right)
2733    }
2734
2735    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2736        settings::update_settings_file::<CollaborationPanelSettings>(
2737            self.fs.clone(),
2738            cx,
2739            move |settings| settings.dock = Some(position),
2740        );
2741    }
2742
2743    fn size(&self, cx: &gpui::WindowContext) -> Pixels {
2744        self.width
2745            .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
2746    }
2747
2748    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
2749        self.width = size;
2750        self.serialize(cx);
2751        cx.notify();
2752    }
2753
2754    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
2755        CollaborationPanelSettings::get_global(cx)
2756            .button
2757            .then(|| ui::IconName::Collab)
2758    }
2759
2760    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
2761        Some("Collab Panel")
2762    }
2763
2764    fn toggle_action(&self) -> Box<dyn gpui::Action> {
2765        Box::new(ToggleFocus)
2766    }
2767
2768    fn persistent_name() -> &'static str {
2769        "CollabPanel"
2770    }
2771}
2772
2773impl FocusableView for CollabPanel {
2774    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
2775        self.filter_editor.focus_handle(cx).clone()
2776    }
2777}
2778
2779impl PartialEq for ListEntry {
2780    fn eq(&self, other: &Self) -> bool {
2781        match self {
2782            ListEntry::Header(section_1) => {
2783                if let ListEntry::Header(section_2) = other {
2784                    return section_1 == section_2;
2785                }
2786            }
2787            ListEntry::CallParticipant { user: user_1, .. } => {
2788                if let ListEntry::CallParticipant { user: user_2, .. } = other {
2789                    return user_1.id == user_2.id;
2790                }
2791            }
2792            ListEntry::ParticipantProject {
2793                project_id: project_id_1,
2794                ..
2795            } => {
2796                if let ListEntry::ParticipantProject {
2797                    project_id: project_id_2,
2798                    ..
2799                } = other
2800                {
2801                    return project_id_1 == project_id_2;
2802                }
2803            }
2804            ListEntry::ParticipantScreen {
2805                peer_id: peer_id_1, ..
2806            } => {
2807                if let ListEntry::ParticipantScreen {
2808                    peer_id: peer_id_2, ..
2809                } = other
2810                {
2811                    return peer_id_1 == peer_id_2;
2812                }
2813            }
2814            ListEntry::Channel {
2815                channel: channel_1, ..
2816            } => {
2817                if let ListEntry::Channel {
2818                    channel: channel_2, ..
2819                } = other
2820                {
2821                    return channel_1.id == channel_2.id;
2822                }
2823            }
2824            ListEntry::ChannelCall { channel_id } => {
2825                if let ListEntry::ChannelCall {
2826                    channel_id: other_id,
2827                } = other
2828                {
2829                    return channel_id == other_id;
2830                }
2831            }
2832            ListEntry::ChannelNotes { channel_id } => {
2833                if let ListEntry::ChannelNotes {
2834                    channel_id: other_id,
2835                } = other
2836                {
2837                    return channel_id == other_id;
2838                }
2839            }
2840            ListEntry::ChannelChat { channel_id } => {
2841                if let ListEntry::ChannelChat {
2842                    channel_id: other_id,
2843                } = other
2844                {
2845                    return channel_id == other_id;
2846                }
2847            }
2848            ListEntry::ChannelInvite(channel_1) => {
2849                if let ListEntry::ChannelInvite(channel_2) = other {
2850                    return channel_1.id == channel_2.id;
2851                }
2852            }
2853            ListEntry::IncomingRequest(user_1) => {
2854                if let ListEntry::IncomingRequest(user_2) = other {
2855                    return user_1.id == user_2.id;
2856                }
2857            }
2858            ListEntry::OutgoingRequest(user_1) => {
2859                if let ListEntry::OutgoingRequest(user_2) = other {
2860                    return user_1.id == user_2.id;
2861                }
2862            }
2863            ListEntry::Contact {
2864                contact: contact_1, ..
2865            } => {
2866                if let ListEntry::Contact {
2867                    contact: contact_2, ..
2868                } = other
2869                {
2870                    return contact_1.user.id == contact_2.user.id;
2871                }
2872            }
2873            ListEntry::ChannelEditor { depth } => {
2874                if let ListEntry::ChannelEditor { depth: other_depth } = other {
2875                    return depth == other_depth;
2876                }
2877            }
2878            ListEntry::ContactPlaceholder => {
2879                if let ListEntry::ContactPlaceholder = other {
2880                    return true;
2881                }
2882            }
2883        }
2884        false
2885    }
2886}
2887
2888struct DraggedChannelView {
2889    channel: Channel,
2890    width: Pixels,
2891}
2892
2893impl Render for DraggedChannelView {
2894    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
2895        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
2896        h_flex()
2897            .font(ui_font)
2898            .bg(cx.theme().colors().background)
2899            .w(self.width)
2900            .p_1()
2901            .gap_1()
2902            .child(
2903                Icon::new(
2904                    if self.channel.visibility == proto::ChannelVisibility::Public {
2905                        IconName::Public
2906                    } else {
2907                        IconName::Hash
2908                    },
2909                )
2910                .size(IconSize::Small)
2911                .color(Color::Muted),
2912            )
2913            .child(Label::new(self.channel.name.clone()))
2914    }
2915}
2916
2917struct JoinChannelTooltip {
2918    channel_store: Model<ChannelStore>,
2919    channel_id: ChannelId,
2920}
2921
2922impl Render for JoinChannelTooltip {
2923    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2924        tooltip_container(cx, |div, cx| {
2925            let participants = self
2926                .channel_store
2927                .read(cx)
2928                .channel_participants(self.channel_id);
2929
2930            div.child(Label::new("Open Channel"))
2931                .children(participants.iter().map(|participant| {
2932                    h_flex()
2933                        .gap_2()
2934                        .child(Avatar::new(participant.avatar_uri.clone()))
2935                        .child(Label::new(participant.github_login.clone()))
2936                }))
2937        })
2938    }
2939}