collab_panel.rs

   1#![allow(unused)]
   2mod channel_modal;
   3mod contact_finder;
   4
   5// use crate::{
   6//     channel_view::{self, ChannelView},
   7//     chat_panel::ChatPanel,
   8//     face_pile::FacePile,
   9//     panel_settings, CollaborationPanelSettings,
  10// };
  11// use anyhow::Result;
  12// use call::ActiveCall;
  13// use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
  14// use channel_modal::ChannelModal;
  15// use client::{
  16//     proto::{self, PeerId},
  17//     Client, Contact, User, UserStore,
  18// };
  19use contact_finder::ContactFinder;
  20use menu::{Cancel, Confirm, SelectNext, SelectPrev};
  21use rpc::proto::{self, PeerId};
  22use theme::{ActiveTheme, ThemeSettings};
  23// use context_menu::{ContextMenu, ContextMenuItem};
  24// use db::kvp::KEY_VALUE_STORE;
  25// use drag_and_drop::{DragAndDrop, Draggable};
  26// use editor::{Cancel, Editor};
  27// use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
  28// use futures::StreamExt;
  29// use fuzzy::{match_strings, StringMatchCandidate};
  30// use gpui::{
  31//     actions,
  32//     elements::{
  33//         Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
  34//         ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
  35//         SafeStylable, Stack, Svg,
  36//     },
  37//     fonts::TextStyle,
  38//     geometry::{
  39//         rect::RectF,
  40//         vector::{vec2f, Vector2F},
  41//     },
  42//     impl_actions,
  43//     platform::{CursorStyle, MouseButton, PromptLevel},
  44//     serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
  45//     ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
  46// };
  47// use menu::{Confirm, SelectNext, SelectPrev};
  48// use project::{Fs, Project};
  49// use serde_derive::{Deserialize, Serialize};
  50// use settings::SettingsStore;
  51// use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
  52// use theme::{components::ComponentExt, IconButton, Interactive};
  53// use util::{maybe, ResultExt, TryFutureExt};
  54// use workspace::{
  55//     dock::{DockPosition, Panel},
  56//     item::ItemHandle,
  57//     FollowNextCollaborator, Workspace,
  58// };
  59
  60// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  61// struct ToggleCollapse {
  62//     location: ChannelId,
  63// }
  64
  65// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  66// struct NewChannel {
  67//     location: ChannelId,
  68// }
  69
  70// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  71// struct RenameChannel {
  72//     channel_id: ChannelId,
  73// }
  74
  75// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  76// struct ToggleSelectedIx {
  77//     ix: usize,
  78// }
  79
  80// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  81// struct RemoveChannel {
  82//     channel_id: ChannelId,
  83// }
  84
  85// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  86// struct InviteMembers {
  87//     channel_id: ChannelId,
  88// }
  89
  90// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  91// struct ManageMembers {
  92//     channel_id: ChannelId,
  93// }
  94
  95#[derive(Action, PartialEq, Debug, Clone, Serialize, Deserialize)]
  96pub struct OpenChannelNotes {
  97    pub channel_id: ChannelId,
  98}
  99
 100// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 101// pub struct JoinChannelCall {
 102//     pub channel_id: u64,
 103// }
 104
 105// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 106// pub struct JoinChannelChat {
 107//     pub channel_id: u64,
 108// }
 109
 110// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 111// pub struct CopyChannelLink {
 112//     pub channel_id: u64,
 113// }
 114
 115// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 116// struct StartMoveChannelFor {
 117//     channel_id: ChannelId,
 118// }
 119
 120// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 121// struct MoveChannel {
 122//     to: ChannelId,
 123// }
 124
 125actions!(
 126    ToggleFocus,
 127    Remove,
 128    Secondary,
 129    CollapseSelectedChannel,
 130    ExpandSelectedChannel,
 131    StartMoveChannel,
 132    MoveSelected,
 133    InsertSpace,
 134);
 135
 136// impl_actions!(
 137//     collab_panel,
 138//     [
 139//         RemoveChannel,
 140//         NewChannel,
 141//         InviteMembers,
 142//         ManageMembers,
 143//         RenameChannel,
 144//         ToggleCollapse,
 145//         OpenChannelNotes,
 146//         JoinChannelCall,
 147//         JoinChannelChat,
 148//         CopyChannelLink,
 149//         StartMoveChannelFor,
 150//         MoveChannel,
 151//         ToggleSelectedIx
 152//     ]
 153// );
 154
 155#[derive(Debug, Copy, Clone, PartialEq, Eq)]
 156struct ChannelMoveClipboard {
 157    channel_id: ChannelId,
 158}
 159
 160const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 161
 162use std::{iter::once, mem, sync::Arc};
 163
 164use call::ActiveCall;
 165use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
 166use client::{Client, Contact, User, UserStore};
 167use db::kvp::KEY_VALUE_STORE;
 168use editor::Editor;
 169use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 170use fuzzy::{match_strings, StringMatchCandidate};
 171use gpui::{
 172    actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action,
 173    AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
 174    FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model,
 175    MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce,
 176    ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext,
 177    VisualContext, WeakView,
 178};
 179use project::{Fs, Project};
 180use serde_derive::{Deserialize, Serialize};
 181use settings::{Settings, SettingsStore};
 182use ui::prelude::*;
 183use ui::{
 184    h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
 185    Label, List, ListHeader, ListItem, Tooltip,
 186};
 187use util::{maybe, ResultExt, TryFutureExt};
 188use workspace::{
 189    dock::{DockPosition, Panel, PanelEvent},
 190    notifications::NotifyResultExt,
 191    Workspace,
 192};
 193
 194use crate::{face_pile::FacePile, CollaborationPanelSettings};
 195
 196use self::channel_modal::ChannelModal;
 197
 198pub fn init(cx: &mut AppContext) {
 199    cx.observe_new_views(|workspace: &mut Workspace, _| {
 200        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
 201            workspace.toggle_panel_focus::<CollabPanel>(cx);
 202        });
 203    })
 204    .detach();
 205    //     contact_finder::init(cx);
 206    //     channel_modal::init(cx);
 207    //     channel_view::init(cx);
 208
 209    //     cx.add_action(CollabPanel::cancel);
 210    //     cx.add_action(CollabPanel::select_next);
 211    //     cx.add_action(CollabPanel::select_prev);
 212    //     cx.add_action(CollabPanel::confirm);
 213    //     cx.add_action(CollabPanel::insert_space);
 214    //     cx.add_action(CollabPanel::remove);
 215    //     cx.add_action(CollabPanel::remove_selected_channel);
 216    //     cx.add_action(CollabPanel::show_inline_context_menu);
 217    //     cx.add_action(CollabPanel::new_subchannel);
 218    //     cx.add_action(CollabPanel::invite_members);
 219    //     cx.add_action(CollabPanel::manage_members);
 220    //     cx.add_action(CollabPanel::rename_selected_channel);
 221    //     cx.add_action(CollabPanel::rename_channel);
 222    //     cx.add_action(CollabPanel::toggle_channel_collapsed_action);
 223    //     cx.add_action(CollabPanel::collapse_selected_channel);
 224    //     cx.add_action(CollabPanel::expand_selected_channel);
 225    //     cx.add_action(CollabPanel::open_channel_notes);
 226    //     cx.add_action(CollabPanel::join_channel_chat);
 227    //     cx.add_action(CollabPanel::copy_channel_link);
 228
 229    //     cx.add_action(
 230    //         |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
 231    //             if panel.selection.take() != Some(action.ix) {
 232    //                 panel.selection = Some(action.ix)
 233    //             }
 234
 235    //             cx.notify();
 236    //         },
 237    //     );
 238
 239    //     cx.add_action(
 240    //         |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
 241    //             let Some(clipboard) = panel.channel_clipboard.take() else {
 242    //                 return;
 243    //             };
 244    //             let Some(selected_channel) = panel.selected_channel() else {
 245    //                 return;
 246    //             };
 247
 248    //             panel
 249    //                 .channel_store
 250    //                 .update(cx, |channel_store, cx| {
 251    //                     channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
 252    //                 })
 253    //                 .detach_and_log_err(cx)
 254    //         },
 255    //     );
 256
 257    //     cx.add_action(
 258    //         |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
 259    //             if let Some(clipboard) = panel.channel_clipboard.take() {
 260    //                 panel.channel_store.update(cx, |channel_store, cx| {
 261    //                     channel_store
 262    //                         .move_channel(clipboard.channel_id, Some(action.to), cx)
 263    //                         .detach_and_log_err(cx)
 264    //                 })
 265    //             }
 266    //         },
 267    //     );
 268}
 269
 270#[derive(Debug)]
 271pub enum ChannelEditingState {
 272    Create {
 273        location: Option<ChannelId>,
 274        pending_name: Option<String>,
 275    },
 276    Rename {
 277        location: ChannelId,
 278        pending_name: Option<String>,
 279    },
 280}
 281
 282impl ChannelEditingState {
 283    fn pending_name(&self) -> Option<String> {
 284        match self {
 285            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
 286            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
 287        }
 288    }
 289}
 290
 291pub struct CollabPanel {
 292    width: Option<Pixels>,
 293    fs: Arc<dyn Fs>,
 294    focus_handle: FocusHandle,
 295    channel_clipboard: Option<ChannelMoveClipboard>,
 296    pending_serialization: Task<Option<()>>,
 297    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
 298    filter_editor: View<Editor>,
 299    channel_name_editor: View<Editor>,
 300    channel_editing_state: Option<ChannelEditingState>,
 301    entries: Vec<ListEntry>,
 302    selection: Option<usize>,
 303    channel_store: Model<ChannelStore>,
 304    user_store: Model<UserStore>,
 305    client: Arc<Client>,
 306    project: Model<Project>,
 307    match_candidates: Vec<StringMatchCandidate>,
 308    scroll_handle: ScrollHandle,
 309    subscriptions: Vec<Subscription>,
 310    collapsed_sections: Vec<Section>,
 311    collapsed_channels: Vec<ChannelId>,
 312    drag_target_channel: ChannelDragTarget,
 313    workspace: WeakView<Workspace>,
 314    // context_menu_on_selected: bool,
 315}
 316
 317#[derive(PartialEq, Eq)]
 318enum ChannelDragTarget {
 319    None,
 320    Root,
 321    Channel(ChannelId),
 322}
 323
 324#[derive(Serialize, Deserialize)]
 325struct SerializedCollabPanel {
 326    width: Option<Pixels>,
 327    collapsed_channels: Option<Vec<u64>>,
 328}
 329
 330// #[derive(Debug)]
 331// pub enum Event {
 332//     DockPositionChanged,
 333//     Focus,
 334//     Dismissed,
 335// }
 336
 337#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 338enum Section {
 339    ActiveCall,
 340    Channels,
 341    ChannelInvites,
 342    ContactRequests,
 343    Contacts,
 344    Online,
 345    Offline,
 346}
 347
 348#[derive(Clone, Debug)]
 349enum ListEntry {
 350    Header(Section),
 351    CallParticipant {
 352        user: Arc<User>,
 353        peer_id: Option<PeerId>,
 354        is_pending: bool,
 355    },
 356    ParticipantProject {
 357        project_id: u64,
 358        worktree_root_names: Vec<String>,
 359        host_user_id: u64,
 360        is_last: bool,
 361    },
 362    ParticipantScreen {
 363        peer_id: Option<PeerId>,
 364        is_last: bool,
 365    },
 366    IncomingRequest(Arc<User>),
 367    OutgoingRequest(Arc<User>),
 368    //     ChannelInvite(Arc<Channel>),
 369    Channel {
 370        channel: Arc<Channel>,
 371        depth: usize,
 372        has_children: bool,
 373    },
 374    ChannelNotes {
 375        channel_id: ChannelId,
 376    },
 377    ChannelChat {
 378        channel_id: ChannelId,
 379    },
 380    ChannelEditor {
 381        depth: usize,
 382    },
 383    Contact {
 384        contact: Arc<Contact>,
 385        calling: bool,
 386    },
 387    ContactPlaceholder,
 388}
 389
 390impl CollabPanel {
 391    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 392        cx.build_view(|cx| {
 393            //             let view_id = cx.view_id();
 394
 395            let filter_editor = cx.build_view(|cx| {
 396                let mut editor = Editor::single_line(cx);
 397                editor.set_placeholder_text("Filter channels, contacts", cx);
 398                editor
 399            });
 400
 401            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
 402                if let editor::EditorEvent::BufferEdited = event {
 403                    let query = this.filter_editor.read(cx).text(cx);
 404                    if !query.is_empty() {
 405                        this.selection.take();
 406                    }
 407                    this.update_entries(true, cx);
 408                    if !query.is_empty() {
 409                        this.selection = this
 410                            .entries
 411                            .iter()
 412                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
 413                    }
 414                } else if let editor::EditorEvent::Blurred = event {
 415                    let query = this.filter_editor.read(cx).text(cx);
 416                    if query.is_empty() {
 417                        this.selection.take();
 418                        this.update_entries(true, cx);
 419                    }
 420                }
 421            })
 422            .detach();
 423
 424            let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx));
 425
 426            cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
 427                if let editor::EditorEvent::Blurred = event {
 428                    if let Some(state) = &this.channel_editing_state {
 429                        if state.pending_name().is_some() {
 430                            return;
 431                        }
 432                    }
 433                    this.take_editing_state(cx);
 434                    this.update_entries(false, cx);
 435                    cx.notify();
 436                }
 437            })
 438            .detach();
 439
 440            //             let list_state =
 441            //                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
 442            //                     let theme = theme::current(cx).clone();
 443            //                     let is_selected = this.selection == Some(ix);
 444            //                     let current_project_id = this.project.read(cx).remote_id();
 445
 446            //                     match &this.entries[ix] {
 447            //                         ListEntry::Header(section) => {
 448            //                             let is_collapsed = this.collapsed_sections.contains(section);
 449            //                             this.render_header(*section, &theme, is_selected, is_collapsed, cx)
 450            //                         }
 451            //                         ListEntry::CallParticipant {
 452            //                             user,
 453            //                             peer_id,
 454            //                             is_pending,
 455            //                         } => Self::render_call_participant(
 456            //                             user,
 457            //                             *peer_id,
 458            //                             this.user_store.clone(),
 459            //                             *is_pending,
 460            //                             is_selected,
 461            //                             &theme,
 462            //                             cx,
 463            //                         ),
 464            //                         ListEntry::ParticipantProject {
 465            //                             project_id,
 466            //                             worktree_root_names,
 467            //                             host_user_id,
 468            //                             is_last,
 469            //                         } => Self::render_participant_project(
 470            //                             *project_id,
 471            //                             worktree_root_names,
 472            //                             *host_user_id,
 473            //                             Some(*project_id) == current_project_id,
 474            //                             *is_last,
 475            //                             is_selected,
 476            //                             &theme,
 477            //                             cx,
 478            //                         ),
 479            //                         ListEntry::ParticipantScreen { peer_id, is_last } => {
 480            //                             Self::render_participant_screen(
 481            //                                 *peer_id,
 482            //                                 *is_last,
 483            //                                 is_selected,
 484            //                                 &theme.collab_panel,
 485            //                                 cx,
 486            //                             )
 487            //                         }
 488            //                         ListEntry::Channel {
 489            //                             channel,
 490            //                             depth,
 491            //                             has_children,
 492            //                         } => {
 493            //                             let channel_row = this.render_channel(
 494            //                                 &*channel,
 495            //                                 *depth,
 496            //                                 &theme,
 497            //                                 is_selected,
 498            //                                 *has_children,
 499            //                                 ix,
 500            //                                 cx,
 501            //                             );
 502
 503            //                             if is_selected && this.context_menu_on_selected {
 504            //                                 Stack::new()
 505            //                                     .with_child(channel_row)
 506            //                                     .with_child(
 507            //                                         ChildView::new(&this.context_menu, cx)
 508            //                                             .aligned()
 509            //                                             .bottom()
 510            //                                             .right(),
 511            //                                     )
 512            //                                     .into_any()
 513            //                             } else {
 514            //                                 return channel_row;
 515            //                             }
 516            //                         }
 517            //                         ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
 518            //                             *channel_id,
 519            //                             &theme.collab_panel,
 520            //                             is_selected,
 521            //                             ix,
 522            //                             cx,
 523            //                         ),
 524            //                         ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
 525            //                             *channel_id,
 526            //                             &theme.collab_panel,
 527            //                             is_selected,
 528            //                             ix,
 529            //                             cx,
 530            //                         ),
 531            //                         ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
 532            //                             channel.clone(),
 533            //                             this.channel_store.clone(),
 534            //                             &theme.collab_panel,
 535            //                             is_selected,
 536            //                             cx,
 537            //                         ),
 538            //                         ListEntry::IncomingRequest(user) => Self::render_contact_request(
 539            //                             user.clone(),
 540            //                             this.user_store.clone(),
 541            //                             &theme.collab_panel,
 542            //                             true,
 543            //                             is_selected,
 544            //                             cx,
 545            //                         ),
 546            //                         ListEntry::OutgoingRequest(user) => Self::render_contact_request(
 547            //                             user.clone(),
 548            //                             this.user_store.clone(),
 549            //                             &theme.collab_panel,
 550            //                             false,
 551            //                             is_selected,
 552            //                             cx,
 553            //                         ),
 554            //                         ListEntry::Contact { contact, calling } => Self::render_contact(
 555            //                             contact,
 556            //                             *calling,
 557            //                             &this.project,
 558            //                             &theme,
 559            //                             is_selected,
 560            //                             cx,
 561            //                         ),
 562            //                         ListEntry::ChannelEditor { depth } => {
 563            //                             this.render_channel_editor(&theme, *depth, cx)
 564            //                         }
 565            //                         ListEntry::ContactPlaceholder => {
 566            //                             this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
 567            //                         }
 568            //                     }
 569            //                 });
 570
 571            let mut this = Self {
 572                width: None,
 573                focus_handle: cx.focus_handle(),
 574                channel_clipboard: None,
 575                fs: workspace.app_state().fs.clone(),
 576                pending_serialization: Task::ready(None),
 577                context_menu: None,
 578                channel_name_editor,
 579                filter_editor,
 580                entries: Vec::default(),
 581                channel_editing_state: None,
 582                selection: None,
 583                channel_store: ChannelStore::global(cx),
 584                user_store: workspace.user_store().clone(),
 585                project: workspace.project().clone(),
 586                subscriptions: Vec::default(),
 587                match_candidates: Vec::default(),
 588                scroll_handle: ScrollHandle::new(),
 589                collapsed_sections: vec![Section::Offline],
 590                collapsed_channels: Vec::default(),
 591                workspace: workspace.weak_handle(),
 592                client: workspace.app_state().client.clone(),
 593                //                 context_menu_on_selected: true,
 594                drag_target_channel: ChannelDragTarget::None,
 595            };
 596
 597            this.update_entries(false, cx);
 598
 599            // Update the dock position when the setting changes.
 600            let mut old_dock_position = this.position(cx);
 601            this.subscriptions.push(cx.observe_global::<SettingsStore>(
 602                move |this: &mut Self, cx| {
 603                    let new_dock_position = this.position(cx);
 604                    if new_dock_position != old_dock_position {
 605                        old_dock_position = new_dock_position;
 606                        cx.emit(PanelEvent::ChangePosition);
 607                    }
 608                    cx.notify();
 609                },
 610            ));
 611
 612            let active_call = ActiveCall::global(cx);
 613            this.subscriptions
 614                .push(cx.observe(&this.user_store, |this, _, cx| {
 615                    this.update_entries(true, cx)
 616                }));
 617            this.subscriptions
 618                .push(cx.observe(&this.channel_store, |this, _, cx| {
 619                    this.update_entries(true, cx)
 620                }));
 621            this.subscriptions
 622                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
 623            this.subscriptions
 624                .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
 625                    this.update_entries(true, cx)
 626                }));
 627            this.subscriptions.push(cx.subscribe(
 628                &this.channel_store,
 629                |this, _channel_store, e, cx| match e {
 630                    ChannelEvent::ChannelCreated(channel_id)
 631                    | ChannelEvent::ChannelRenamed(channel_id) => {
 632                        if this.take_editing_state(cx) {
 633                            this.update_entries(false, cx);
 634                            this.selection = this.entries.iter().position(|entry| {
 635                                if let ListEntry::Channel { channel, .. } = entry {
 636                                    channel.id == *channel_id
 637                                } else {
 638                                    false
 639                                }
 640                            });
 641                        }
 642                    }
 643                },
 644            ));
 645
 646            this
 647        })
 648    }
 649
 650    fn contacts(&self, cx: &AppContext) -> Option<Vec<Arc<Contact>>> {
 651        Some(self.user_store.read(cx).contacts().to_owned())
 652    }
 653    pub async fn load(
 654        workspace: WeakView<Workspace>,
 655        mut cx: AsyncWindowContext,
 656    ) -> anyhow::Result<View<Self>> {
 657        let serialized_panel = cx
 658            .background_executor()
 659            .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
 660            .await
 661            .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
 662            .log_err()
 663            .flatten()
 664            .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
 665            .transpose()
 666            .log_err()
 667            .flatten();
 668
 669        workspace.update(&mut cx, |workspace, cx| {
 670            let panel = CollabPanel::new(workspace, cx);
 671            if let Some(serialized_panel) = serialized_panel {
 672                panel.update(cx, |panel, cx| {
 673                    panel.width = serialized_panel.width;
 674                    panel.collapsed_channels = serialized_panel
 675                        .collapsed_channels
 676                        .unwrap_or_else(|| Vec::new());
 677                    cx.notify();
 678                });
 679            }
 680            panel
 681        })
 682    }
 683
 684    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 685        let width = self.width;
 686        let collapsed_channels = self.collapsed_channels.clone();
 687        self.pending_serialization = cx.background_executor().spawn(
 688            async move {
 689                KEY_VALUE_STORE
 690                    .write_kvp(
 691                        COLLABORATION_PANEL_KEY.into(),
 692                        serde_json::to_string(&SerializedCollabPanel {
 693                            width,
 694                            collapsed_channels: Some(collapsed_channels),
 695                        })?,
 696                    )
 697                    .await?;
 698                anyhow::Ok(())
 699            }
 700            .log_err(),
 701        );
 702    }
 703
 704    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
 705        let channel_store = self.channel_store.read(cx);
 706        let user_store = self.user_store.read(cx);
 707        let query = self.filter_editor.read(cx).text(cx);
 708        let executor = cx.background_executor().clone();
 709
 710        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 711        let old_entries = mem::take(&mut self.entries);
 712        let mut scroll_to_top = false;
 713
 714        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 715            self.entries.push(ListEntry::Header(Section::ActiveCall));
 716            if !old_entries
 717                .iter()
 718                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
 719            {
 720                scroll_to_top = true;
 721            }
 722
 723            if !self.collapsed_sections.contains(&Section::ActiveCall) {
 724                let room = room.read(cx);
 725
 726                if let Some(channel_id) = room.channel_id() {
 727                    self.entries.push(ListEntry::ChannelNotes { channel_id });
 728                    self.entries.push(ListEntry::ChannelChat { channel_id })
 729                }
 730
 731                // Populate the active user.
 732                if let Some(user) = user_store.current_user() {
 733                    self.match_candidates.clear();
 734                    self.match_candidates.push(StringMatchCandidate {
 735                        id: 0,
 736                        string: user.github_login.clone(),
 737                        char_bag: user.github_login.chars().collect(),
 738                    });
 739                    let matches = executor.block(match_strings(
 740                        &self.match_candidates,
 741                        &query,
 742                        true,
 743                        usize::MAX,
 744                        &Default::default(),
 745                        executor.clone(),
 746                    ));
 747                    if !matches.is_empty() {
 748                        let user_id = user.id;
 749                        self.entries.push(ListEntry::CallParticipant {
 750                            user,
 751                            peer_id: None,
 752                            is_pending: false,
 753                        });
 754                        let mut projects = room.local_participant().projects.iter().peekable();
 755                        while let Some(project) = projects.next() {
 756                            self.entries.push(ListEntry::ParticipantProject {
 757                                project_id: project.id,
 758                                worktree_root_names: project.worktree_root_names.clone(),
 759                                host_user_id: user_id,
 760                                is_last: projects.peek().is_none() && !room.is_screen_sharing(),
 761                            });
 762                        }
 763                        if room.is_screen_sharing() {
 764                            self.entries.push(ListEntry::ParticipantScreen {
 765                                peer_id: None,
 766                                is_last: true,
 767                            });
 768                        }
 769                    }
 770                }
 771
 772                // Populate remote participants.
 773                self.match_candidates.clear();
 774                self.match_candidates
 775                    .extend(room.remote_participants().iter().map(|(_, participant)| {
 776                        StringMatchCandidate {
 777                            id: participant.user.id as usize,
 778                            string: participant.user.github_login.clone(),
 779                            char_bag: participant.user.github_login.chars().collect(),
 780                        }
 781                    }));
 782                let matches = executor.block(match_strings(
 783                    &self.match_candidates,
 784                    &query,
 785                    true,
 786                    usize::MAX,
 787                    &Default::default(),
 788                    executor.clone(),
 789                ));
 790                for mat in matches {
 791                    let user_id = mat.candidate_id as u64;
 792                    let participant = &room.remote_participants()[&user_id];
 793                    self.entries.push(ListEntry::CallParticipant {
 794                        user: participant.user.clone(),
 795                        peer_id: Some(participant.peer_id),
 796                        is_pending: false,
 797                    });
 798                    let mut projects = participant.projects.iter().peekable();
 799                    while let Some(project) = projects.next() {
 800                        self.entries.push(ListEntry::ParticipantProject {
 801                            project_id: project.id,
 802                            worktree_root_names: project.worktree_root_names.clone(),
 803                            host_user_id: participant.user.id,
 804                            is_last: projects.peek().is_none()
 805                                && participant.video_tracks.is_empty(),
 806                        });
 807                    }
 808                    if !participant.video_tracks.is_empty() {
 809                        self.entries.push(ListEntry::ParticipantScreen {
 810                            peer_id: Some(participant.peer_id),
 811                            is_last: true,
 812                        });
 813                    }
 814                }
 815
 816                // Populate pending participants.
 817                self.match_candidates.clear();
 818                self.match_candidates
 819                    .extend(room.pending_participants().iter().enumerate().map(
 820                        |(id, participant)| StringMatchCandidate {
 821                            id,
 822                            string: participant.github_login.clone(),
 823                            char_bag: participant.github_login.chars().collect(),
 824                        },
 825                    ));
 826                let matches = executor.block(match_strings(
 827                    &self.match_candidates,
 828                    &query,
 829                    true,
 830                    usize::MAX,
 831                    &Default::default(),
 832                    executor.clone(),
 833                ));
 834                self.entries
 835                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
 836                        user: room.pending_participants()[mat.candidate_id].clone(),
 837                        peer_id: None,
 838                        is_pending: true,
 839                    }));
 840            }
 841        }
 842
 843        let mut request_entries = Vec::new();
 844
 845        if cx.has_flag::<ChannelsAlpha>() {
 846            self.entries.push(ListEntry::Header(Section::Channels));
 847
 848            if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
 849                self.match_candidates.clear();
 850                self.match_candidates
 851                    .extend(channel_store.ordered_channels().enumerate().map(
 852                        |(ix, (_, channel))| StringMatchCandidate {
 853                            id: ix,
 854                            string: channel.name.clone(),
 855                            char_bag: channel.name.chars().collect(),
 856                        },
 857                    ));
 858                let matches = executor.block(match_strings(
 859                    &self.match_candidates,
 860                    &query,
 861                    true,
 862                    usize::MAX,
 863                    &Default::default(),
 864                    executor.clone(),
 865                ));
 866                if let Some(state) = &self.channel_editing_state {
 867                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
 868                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
 869                    }
 870                }
 871                let mut collapse_depth = None;
 872                for mat in matches {
 873                    let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
 874                    let depth = channel.parent_path.len();
 875
 876                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
 877                        collapse_depth = Some(depth);
 878                    } else if let Some(collapsed_depth) = collapse_depth {
 879                        if depth > collapsed_depth {
 880                            continue;
 881                        }
 882                        if self.is_channel_collapsed(channel.id) {
 883                            collapse_depth = Some(depth);
 884                        } else {
 885                            collapse_depth = None;
 886                        }
 887                    }
 888
 889                    let has_children = channel_store
 890                        .channel_at_index(mat.candidate_id + 1)
 891                        .map_or(false, |next_channel| {
 892                            next_channel.parent_path.ends_with(&[channel.id])
 893                        });
 894
 895                    match &self.channel_editing_state {
 896                        Some(ChannelEditingState::Create {
 897                            location: parent_id,
 898                            ..
 899                        }) if *parent_id == Some(channel.id) => {
 900                            self.entries.push(ListEntry::Channel {
 901                                channel: channel.clone(),
 902                                depth,
 903                                has_children: false,
 904                            });
 905                            self.entries
 906                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
 907                        }
 908                        Some(ChannelEditingState::Rename {
 909                            location: parent_id,
 910                            ..
 911                        }) if parent_id == &channel.id => {
 912                            self.entries.push(ListEntry::ChannelEditor { depth });
 913                        }
 914                        _ => {
 915                            self.entries.push(ListEntry::Channel {
 916                                channel: channel.clone(),
 917                                depth,
 918                                has_children,
 919                            });
 920                        }
 921                    }
 922                }
 923            }
 924
 925            //             let channel_invites = channel_store.channel_invitations();
 926            //             if !channel_invites.is_empty() {
 927            //                 self.match_candidates.clear();
 928            //                 self.match_candidates
 929            //                     .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
 930            //                         StringMatchCandidate {
 931            //                             id: ix,
 932            //                             string: channel.name.clone(),
 933            //                             char_bag: channel.name.chars().collect(),
 934            //                         }
 935            //                     }));
 936            //                 let matches = executor.block(match_strings(
 937            //                     &self.match_candidates,
 938            //                     &query,
 939            //                     true,
 940            //                     usize::MAX,
 941            //                     &Default::default(),
 942            //                     executor.clone(),
 943            //                 ));
 944            //                 request_entries.extend(matches.iter().map(|mat| {
 945            //                     ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
 946            //                 }));
 947
 948            //                 if !request_entries.is_empty() {
 949            //                     self.entries
 950            //                         .push(ListEntry::Header(Section::ChannelInvites));
 951            //                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
 952            //                         self.entries.append(&mut request_entries);
 953            //                     }
 954            //                 }
 955            //             }
 956        }
 957
 958        self.entries.push(ListEntry::Header(Section::Contacts));
 959
 960        request_entries.clear();
 961        let incoming = user_store.incoming_contact_requests();
 962        if !incoming.is_empty() {
 963            self.match_candidates.clear();
 964            self.match_candidates
 965                .extend(
 966                    incoming
 967                        .iter()
 968                        .enumerate()
 969                        .map(|(ix, user)| StringMatchCandidate {
 970                            id: ix,
 971                            string: user.github_login.clone(),
 972                            char_bag: user.github_login.chars().collect(),
 973                        }),
 974                );
 975            let matches = executor.block(match_strings(
 976                &self.match_candidates,
 977                &query,
 978                true,
 979                usize::MAX,
 980                &Default::default(),
 981                executor.clone(),
 982            ));
 983            request_entries.extend(
 984                matches
 985                    .iter()
 986                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
 987            );
 988        }
 989
 990        let outgoing = user_store.outgoing_contact_requests();
 991        if !outgoing.is_empty() {
 992            self.match_candidates.clear();
 993            self.match_candidates
 994                .extend(
 995                    outgoing
 996                        .iter()
 997                        .enumerate()
 998                        .map(|(ix, user)| StringMatchCandidate {
 999                            id: ix,
1000                            string: user.github_login.clone(),
1001                            char_bag: user.github_login.chars().collect(),
1002                        }),
1003                );
1004            let matches = executor.block(match_strings(
1005                &self.match_candidates,
1006                &query,
1007                true,
1008                usize::MAX,
1009                &Default::default(),
1010                executor.clone(),
1011            ));
1012            request_entries.extend(
1013                matches
1014                    .iter()
1015                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
1016            );
1017        }
1018
1019        if !request_entries.is_empty() {
1020            self.entries
1021                .push(ListEntry::Header(Section::ContactRequests));
1022            if !self.collapsed_sections.contains(&Section::ContactRequests) {
1023                self.entries.append(&mut request_entries);
1024            }
1025        }
1026
1027        let contacts = user_store.contacts();
1028        if !contacts.is_empty() {
1029            self.match_candidates.clear();
1030            self.match_candidates
1031                .extend(
1032                    contacts
1033                        .iter()
1034                        .enumerate()
1035                        .map(|(ix, contact)| StringMatchCandidate {
1036                            id: ix,
1037                            string: contact.user.github_login.clone(),
1038                            char_bag: contact.user.github_login.chars().collect(),
1039                        }),
1040                );
1041
1042            let matches = executor.block(match_strings(
1043                &self.match_candidates,
1044                &query,
1045                true,
1046                usize::MAX,
1047                &Default::default(),
1048                executor.clone(),
1049            ));
1050
1051            let (online_contacts, offline_contacts) = matches
1052                .iter()
1053                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
1054
1055            for (matches, section) in [
1056                (online_contacts, Section::Online),
1057                (offline_contacts, Section::Offline),
1058            ] {
1059                if !matches.is_empty() {
1060                    self.entries.push(ListEntry::Header(section));
1061                    if !self.collapsed_sections.contains(&section) {
1062                        let active_call = &ActiveCall::global(cx).read(cx);
1063                        for mat in matches {
1064                            let contact = &contacts[mat.candidate_id];
1065                            self.entries.push(ListEntry::Contact {
1066                                contact: contact.clone(),
1067                                calling: active_call.pending_invites().contains(&contact.user.id),
1068                            });
1069                        }
1070                    }
1071                }
1072            }
1073        }
1074
1075        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
1076            self.entries.push(ListEntry::ContactPlaceholder);
1077        }
1078
1079        if select_same_item {
1080            if let Some(prev_selected_entry) = prev_selected_entry {
1081                self.selection.take();
1082                for (ix, entry) in self.entries.iter().enumerate() {
1083                    if *entry == prev_selected_entry {
1084                        self.selection = Some(ix);
1085                        self.scroll_handle.scroll_to_item(ix);
1086                        break;
1087                    }
1088                }
1089            }
1090        } else {
1091            self.selection = self.selection.and_then(|prev_selection| {
1092                if self.entries.is_empty() {
1093                    None
1094                } else {
1095                    let ix = prev_selection.min(self.entries.len() - 1);
1096                    self.scroll_handle.scroll_to_item(ix);
1097                    Some(ix)
1098                }
1099            });
1100        }
1101
1102        if scroll_to_top {
1103            self.scroll_handle.scroll_to_item(0)
1104        } else {
1105            let (old_index, old_offset) = self.scroll_handle.logical_scroll_top();
1106            // Attempt to maintain the same scroll position.
1107            if let Some(old_top_entry) = old_entries.get(old_index) {
1108                let (new_index, new_offset) = self
1109                    .entries
1110                    .iter()
1111                    .position(|entry| entry == old_top_entry)
1112                    .map(|item_ix| (item_ix, old_offset))
1113                    .or_else(|| {
1114                        let entry_after_old_top = old_entries.get(old_index + 1)?;
1115                        let item_ix = self
1116                            .entries
1117                            .iter()
1118                            .position(|entry| entry == entry_after_old_top)?;
1119                        Some((item_ix, px(0.)))
1120                    })
1121                    .or_else(|| {
1122                        let entry_before_old_top = old_entries.get(old_index.saturating_sub(1))?;
1123                        let item_ix = self
1124                            .entries
1125                            .iter()
1126                            .position(|entry| entry == entry_before_old_top)?;
1127                        Some((item_ix, px(0.)))
1128                    })
1129                    .unwrap_or_else(|| (old_index, old_offset));
1130
1131                self.scroll_handle
1132                    .set_logical_scroll_top(new_index, new_offset);
1133            }
1134        }
1135
1136        cx.notify();
1137    }
1138
1139    fn render_call_participant(
1140        &self,
1141        user: Arc<User>,
1142        peer_id: Option<PeerId>,
1143        is_pending: bool,
1144        cx: &mut ViewContext<Self>,
1145    ) -> impl IntoElement {
1146        let is_current_user =
1147            self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
1148        let tooltip = format!("Follow {}", user.github_login);
1149
1150        ListItem::new(SharedString::from(user.github_login.clone()))
1151            .left_child(Avatar::data(user.avatar.clone().unwrap()))
1152            .child(
1153                h_stack()
1154                    .w_full()
1155                    .justify_between()
1156                    .child(Label::new(user.github_login.clone()))
1157                    .child(if is_pending {
1158                        Label::new("Calling").color(Color::Muted).into_any_element()
1159                    } else if is_current_user {
1160                        IconButton::new("leave-call", Icon::ArrowRight)
1161                            .on_click(cx.listener(move |this, _, cx| {
1162                                Self::leave_call(cx);
1163                            }))
1164                            .tooltip(|cx| Tooltip::text("Leave Call", cx))
1165                            .into_any_element()
1166                    } else {
1167                        div().into_any_element()
1168                    }),
1169            )
1170            .when_some(peer_id, |this, peer_id| {
1171                this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
1172                    .on_click(cx.listener(move |this, _, cx| {
1173                        this.workspace
1174                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx));
1175                    }))
1176            })
1177    }
1178
1179    fn render_participant_project(
1180        &self,
1181        project_id: u64,
1182        worktree_root_names: &[String],
1183        host_user_id: u64,
1184        //         is_current: bool,
1185        is_last: bool,
1186        //         is_selected: bool,
1187        //         theme: &theme::Theme,
1188        cx: &mut ViewContext<Self>,
1189    ) -> impl IntoElement {
1190        let project_name: SharedString = if worktree_root_names.is_empty() {
1191            "untitled".to_string()
1192        } else {
1193            worktree_root_names.join(", ")
1194        }
1195        .into();
1196
1197        let theme = cx.theme();
1198
1199        ListItem::new(project_id as usize)
1200            .on_click(cx.listener(move |this, _, cx| {
1201                this.workspace.update(cx, |workspace, cx| {
1202                    let app_state = workspace.app_state().clone();
1203                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1204                        .detach_and_log_err(cx);
1205                });
1206            }))
1207            .left_child(IconButton::new(0, Icon::Folder))
1208            .child(
1209                h_stack()
1210                    .w_full()
1211                    .justify_between()
1212                    .child(render_tree_branch(is_last, cx))
1213                    .child(Label::new(project_name.clone())),
1214            )
1215            .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
1216
1217        //         enum JoinProject {}
1218        //         enum JoinProjectTooltip {}
1219
1220        //         let collab_theme = &theme.collab_panel;
1221        //         let host_avatar_width = collab_theme
1222        //             .contact_avatar
1223        //             .width
1224        //             .or(collab_theme.contact_avatar.height)
1225        //             .unwrap_or(0.);
1226        //         let tree_branch = collab_theme.tree_branch;
1227
1228        //         let content =
1229        //             MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
1230        //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1231        //                 let row = if is_current {
1232        //                     collab_theme
1233        //                         .project_row
1234        //                         .in_state(true)
1235        //                         .style_for(&mut Default::default())
1236        //                 } else {
1237        //                     collab_theme
1238        //                         .project_row
1239        //                         .in_state(is_selected)
1240        //                         .style_for(mouse_state)
1241        //                 };
1242
1243        //                 Flex::row()
1244        //                     .with_child(render_tree_branch(
1245        //                         tree_branch,
1246        //                         &row.name.text,
1247        //                         is_last,
1248        //                         vec2f(host_avatar_width, collab_theme.row_height),
1249        //                         cx.font_cache(),
1250        //                     ))
1251        //                     .with_child(
1252        //                         Svg::new("icons/file_icons/folder.svg")
1253        //                             .with_color(collab_theme.channel_hash.color)
1254        //                             .constrained()
1255        //                             .with_width(collab_theme.channel_hash.width)
1256        //                             .aligned()
1257        //                             .left(),
1258        //                     )
1259        //                     .with_child(
1260        //                         Label::new(project_name.clone(), row.name.text.clone())
1261        //                             .aligned()
1262        //                             .left()
1263        //                             .contained()
1264        //                             .with_style(row.name.container)
1265        //                             .flex(1., false),
1266        //                     )
1267        //                     .constrained()
1268        //                     .with_height(collab_theme.row_height)
1269        //                     .contained()
1270        //                     .with_style(row.container)
1271        //             });
1272
1273        //         if is_current {
1274        //             return content.into_any();
1275        //         }
1276
1277        //         content
1278        //             .with_cursor_style(CursorStyle::PointingHand)
1279        //             .on_click(MouseButton::Left, move |_, this, cx| {
1280        //                 if let Some(workspace) = this.workspace.upgrade(cx) {
1281        //                     let app_state = workspace.read(cx).app_state().clone();
1282        //                     workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1283        //                         .detach_and_log_err(cx);
1284        //                 }
1285        //             })
1286        //             .with_tooltip::<JoinProjectTooltip>(
1287        //                 project_id as usize,
1288        //                 format!("Open {}", project_name),
1289        //                 None,
1290        //                 theme.tooltip.clone(),
1291        //                 cx,
1292        //             )
1293        //             .into_any()
1294    }
1295
1296    fn render_participant_screen(
1297        &self,
1298        peer_id: Option<PeerId>,
1299        is_last: bool,
1300        cx: &mut ViewContext<Self>,
1301    ) -> impl IntoElement {
1302        //         enum OpenSharedScreen {}
1303
1304        //         let host_avatar_width = theme
1305        //             .contact_avatar
1306        //             .width
1307        //             .or(theme.contact_avatar.height)
1308        //             .unwrap_or(0.);
1309        //         let tree_branch = theme.tree_branch;
1310
1311        //         let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
1312        //             peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
1313        //             cx,
1314        //             |mouse_state, cx| {
1315        //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1316        //                 let row = theme
1317        //                     .project_row
1318        //                     .in_state(is_selected)
1319        //                     .style_for(mouse_state);
1320
1321        //                 Flex::row()
1322        //                     .with_child(render_tree_branch(
1323        //                         tree_branch,
1324        //                         &row.name.text,
1325        //                         is_last,
1326        //                         vec2f(host_avatar_width, theme.row_height),
1327        //                         cx.font_cache(),
1328        //                     ))
1329        //                     .with_child(
1330        //                         Svg::new("icons/desktop.svg")
1331        //                             .with_color(theme.channel_hash.color)
1332        //                             .constrained()
1333        //                             .with_width(theme.channel_hash.width)
1334        //                             .aligned()
1335        //                             .left(),
1336        //                     )
1337        //                     .with_child(
1338        //                         Label::new("Screen", row.name.text.clone())
1339        //                             .aligned()
1340        //                             .left()
1341        //                             .contained()
1342        //                             .with_style(row.name.container)
1343        //                             .flex(1., false),
1344        //                     )
1345        //                     .constrained()
1346        //                     .with_height(theme.row_height)
1347        //                     .contained()
1348        //                     .with_style(row.container)
1349        //             },
1350        //         );
1351        //         if peer_id.is_none() {
1352        //             return handler.into_any();
1353        //         }
1354        //         handler
1355        //             .with_cursor_style(CursorStyle::PointingHand)
1356        //             .on_click(MouseButton::Left, move |_, this, cx| {
1357        //                 if let Some(workspace) = this.workspace.upgrade(cx) {
1358        //                     workspace.update(cx, |workspace, cx| {
1359        //                         workspace.open_shared_screen(peer_id.unwrap(), cx)
1360        //                     });
1361        //                 }
1362        //             })
1363        //             .into_any()
1364
1365        div()
1366    }
1367
1368    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1369        if let Some(_) = self.channel_editing_state.take() {
1370            self.channel_name_editor.update(cx, |editor, cx| {
1371                editor.set_text("", cx);
1372            });
1373            true
1374        } else {
1375            false
1376        }
1377    }
1378
1379    //     fn render_contact_placeholder(
1380    //         &self,
1381    //         theme: &theme::CollabPanel,
1382    //         is_selected: bool,
1383    //         cx: &mut ViewContext<Self>,
1384    //     ) -> AnyElement<Self> {
1385    //         enum AddContacts {}
1386    //         MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1387    //             let style = theme.list_empty_state.style_for(is_selected, state);
1388    //             Flex::row()
1389    //                 .with_child(
1390    //                     Svg::new("icons/plus.svg")
1391    //                         .with_color(theme.list_empty_icon.color)
1392    //                         .constrained()
1393    //                         .with_width(theme.list_empty_icon.width)
1394    //                         .aligned()
1395    //                         .left(),
1396    //                 )
1397    //                 .with_child(
1398    //                     Label::new("Add a contact", style.text.clone())
1399    //                         .contained()
1400    //                         .with_style(theme.list_empty_label_container),
1401    //                 )
1402    //                 .align_children_center()
1403    //                 .contained()
1404    //                 .with_style(style.container)
1405    //                 .into_any()
1406    //         })
1407    //         .on_click(MouseButton::Left, |_, this, cx| {
1408    //             this.toggle_contact_finder(cx);
1409    //         })
1410    //         .into_any()
1411    //     }
1412
1413    fn render_channel_notes(
1414        &self,
1415        channel_id: ChannelId,
1416        cx: &mut ViewContext<Self>,
1417    ) -> impl IntoElement {
1418        //         enum ChannelNotes {}
1419        //         let host_avatar_width = theme
1420        //             .contact_avatar
1421        //             .width
1422        //             .or(theme.contact_avatar.height)
1423        //             .unwrap_or(0.);
1424
1425        //         MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
1426        //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
1427        //             let row = theme.project_row.in_state(is_selected).style_for(state);
1428
1429        //             Flex::<Self>::row()
1430        //                 .with_child(render_tree_branch(
1431        //                     tree_branch,
1432        //                     &row.name.text,
1433        //                     false,
1434        //                     vec2f(host_avatar_width, theme.row_height),
1435        //                     cx.font_cache(),
1436        //                 ))
1437        //                 .with_child(
1438        //                     Svg::new("icons/file.svg")
1439        //                         .with_color(theme.channel_hash.color)
1440        //                         .constrained()
1441        //                         .with_width(theme.channel_hash.width)
1442        //                         .aligned()
1443        //                         .left(),
1444        //                 )
1445        //                 .with_child(
1446        //                     Label::new("notes", theme.channel_name.text.clone())
1447        //                         .contained()
1448        //                         .with_style(theme.channel_name.container)
1449        //                         .aligned()
1450        //                         .left()
1451        //                         .flex(1., true),
1452        //                 )
1453        //                 .constrained()
1454        //                 .with_height(theme.row_height)
1455        //                 .contained()
1456        //                 .with_style(*theme.channel_row.style_for(is_selected, state))
1457        //                 .with_padding_left(theme.channel_row.default_style().padding.left)
1458        //         })
1459        //         .on_click(MouseButton::Left, move |_, this, cx| {
1460        //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
1461        //         })
1462        //         .with_cursor_style(CursorStyle::PointingHand)
1463        //         .into_any()
1464
1465        div()
1466    }
1467
1468    fn render_channel_chat(
1469        &self,
1470        channel_id: ChannelId,
1471        cx: &mut ViewContext<Self>,
1472    ) -> impl IntoElement {
1473        //         enum ChannelChat {}
1474        //         let host_avatar_width = theme
1475        //             .contact_avatar
1476        //             .width
1477        //             .or(theme.contact_avatar.height)
1478        //             .unwrap_or(0.);
1479
1480        //         MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
1481        //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
1482        //             let row = theme.project_row.in_state(is_selected).style_for(state);
1483
1484        //             Flex::<Self>::row()
1485        //                 .with_child(render_tree_branch(
1486        //                     tree_branch,
1487        //                     &row.name.text,
1488        //                     true,
1489        //                     vec2f(host_avatar_width, theme.row_height),
1490        //                     cx.font_cache(),
1491        //                 ))
1492        //                 .with_child(
1493        //                     Svg::new("icons/conversations.svg")
1494        //                         .with_color(theme.channel_hash.color)
1495        //                         .constrained()
1496        //                         .with_width(theme.channel_hash.width)
1497        //                         .aligned()
1498        //                         .left(),
1499        //                 )
1500        //                 .with_child(
1501        //                     Label::new("chat", theme.channel_name.text.clone())
1502        //                         .contained()
1503        //                         .with_style(theme.channel_name.container)
1504        //                         .aligned()
1505        //                         .left()
1506        //                         .flex(1., true),
1507        //                 )
1508        //                 .constrained()
1509        //                 .with_height(theme.row_height)
1510        //                 .contained()
1511        //                 .with_style(*theme.channel_row.style_for(is_selected, state))
1512        //                 .with_padding_left(theme.channel_row.default_style().padding.left)
1513        //         })
1514        //         .on_click(MouseButton::Left, move |_, this, cx| {
1515        //             this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
1516        //         })
1517        //         .with_cursor_style(CursorStyle::PointingHand)
1518        //         .into_any()
1519        div()
1520    }
1521
1522    //     fn render_channel_invite(
1523    //         channel: Arc<Channel>,
1524    //         channel_store: ModelHandle<ChannelStore>,
1525    //         theme: &theme::CollabPanel,
1526    //         is_selected: bool,
1527    //         cx: &mut ViewContext<Self>,
1528    //     ) -> AnyElement<Self> {
1529    //         enum Decline {}
1530    //         enum Accept {}
1531
1532    //         let channel_id = channel.id;
1533    //         let is_invite_pending = channel_store
1534    //             .read(cx)
1535    //             .has_pending_channel_invite_response(&channel);
1536    //         let button_spacing = theme.contact_button_spacing;
1537
1538    //         Flex::row()
1539    //             .with_child(
1540    //                 Svg::new("icons/hash.svg")
1541    //                     .with_color(theme.channel_hash.color)
1542    //                     .constrained()
1543    //                     .with_width(theme.channel_hash.width)
1544    //                     .aligned()
1545    //                     .left(),
1546    //             )
1547    //             .with_child(
1548    //                 Label::new(channel.name.clone(), theme.contact_username.text.clone())
1549    //                     .contained()
1550    //                     .with_style(theme.contact_username.container)
1551    //                     .aligned()
1552    //                     .left()
1553    //                     .flex(1., true),
1554    //             )
1555    //             .with_child(
1556    //                 MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
1557    //                     let button_style = if is_invite_pending {
1558    //                         &theme.disabled_button
1559    //                     } else {
1560    //                         theme.contact_button.style_for(mouse_state)
1561    //                     };
1562    //                     render_icon_button(button_style, "icons/x.svg").aligned()
1563    //                 })
1564    //                 .with_cursor_style(CursorStyle::PointingHand)
1565    //                 .on_click(MouseButton::Left, move |_, this, cx| {
1566    //                     this.respond_to_channel_invite(channel_id, false, cx);
1567    //                 })
1568    //                 .contained()
1569    //                 .with_margin_right(button_spacing),
1570    //             )
1571    //             .with_child(
1572    //                 MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
1573    //                     let button_style = if is_invite_pending {
1574    //                         &theme.disabled_button
1575    //                     } else {
1576    //                         theme.contact_button.style_for(mouse_state)
1577    //                     };
1578    //                     render_icon_button(button_style, "icons/check.svg")
1579    //                         .aligned()
1580    //                         .flex_float()
1581    //                 })
1582    //                 .with_cursor_style(CursorStyle::PointingHand)
1583    //                 .on_click(MouseButton::Left, move |_, this, cx| {
1584    //                     this.respond_to_channel_invite(channel_id, true, cx);
1585    //                 }),
1586    //             )
1587    //             .constrained()
1588    //             .with_height(theme.row_height)
1589    //             .contained()
1590    //             .with_style(
1591    //                 *theme
1592    //                     .contact_row
1593    //                     .in_state(is_selected)
1594    //                     .style_for(&mut Default::default()),
1595    //             )
1596    //             .with_padding_left(
1597    //                 theme.contact_row.default_style().padding.left + theme.channel_indent,
1598    //             )
1599    //             .into_any()
1600    //     }
1601
1602    fn has_subchannels(&self, ix: usize) -> bool {
1603        self.entries.get(ix).map_or(false, |entry| {
1604            if let ListEntry::Channel { has_children, .. } = entry {
1605                *has_children
1606            } else {
1607                false
1608            }
1609        })
1610    }
1611
1612    fn deploy_channel_context_menu(
1613        &mut self,
1614        position: Point<Pixels>,
1615        channel_id: ChannelId,
1616        ix: usize,
1617        cx: &mut ViewContext<Self>,
1618    ) {
1619        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
1620            self.channel_store
1621                .read(cx)
1622                .channel_for_id(clipboard.channel_id)
1623                .map(|channel| channel.name.clone())
1624        });
1625        let this = cx.view().clone();
1626
1627        let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
1628            if self.has_subchannels(ix) {
1629                let expand_action_name = if self.is_channel_collapsed(channel_id) {
1630                    "Expand Subchannels"
1631                } else {
1632                    "Collapse Subchannels"
1633                };
1634                context_menu = context_menu.entry(
1635                    expand_action_name,
1636                    cx.handler_for(&this, move |this, cx| {
1637                        this.toggle_channel_collapsed(channel_id, cx)
1638                    }),
1639                );
1640            }
1641
1642            context_menu = context_menu
1643                .entry(
1644                    "Open Notes",
1645                    cx.handler_for(&this, move |this, cx| {
1646                        this.open_channel_notes(channel_id, cx)
1647                    }),
1648                )
1649                .entry(
1650                    "Open Chat",
1651                    cx.handler_for(&this, move |this, cx| {
1652                        this.join_channel_chat(channel_id, cx)
1653                    }),
1654                )
1655                .entry(
1656                    "Copy Channel Link",
1657                    cx.handler_for(&this, move |this, cx| {
1658                        this.copy_channel_link(channel_id, cx)
1659                    }),
1660                );
1661
1662            if self.channel_store.read(cx).is_channel_admin(channel_id) {
1663                context_menu = context_menu
1664                    .separator()
1665                    .entry(
1666                        "New Subchannel",
1667                        cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
1668                    )
1669                    .entry(
1670                        "Rename",
1671                        cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
1672                    )
1673                    .entry(
1674                        "Move this channel",
1675                        cx.handler_for(&this, move |this, cx| {
1676                            this.start_move_channel(channel_id, cx)
1677                        }),
1678                    );
1679
1680                if let Some(channel_name) = clipboard_channel_name {
1681                    context_menu = context_menu.separator().entry(
1682                        format!("Move '#{}' here", channel_name),
1683                        cx.handler_for(&this, move |this, cx| {
1684                            this.move_channel_on_clipboard(channel_id, cx)
1685                        }),
1686                    );
1687                }
1688
1689                context_menu = context_menu
1690                    .separator()
1691                    .entry(
1692                        "Invite Members",
1693                        cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
1694                    )
1695                    .entry(
1696                        "Manage Members",
1697                        cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
1698                    )
1699                    .entry(
1700                        "Delete",
1701                        cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
1702                    );
1703            }
1704
1705            context_menu
1706        });
1707
1708        cx.focus_view(&context_menu);
1709        let subscription =
1710            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1711                if this.context_menu.as_ref().is_some_and(|context_menu| {
1712                    context_menu.0.focus_handle(cx).contains_focused(cx)
1713                }) {
1714                    cx.focus_self();
1715                }
1716                this.context_menu.take();
1717                cx.notify();
1718            });
1719        self.context_menu = Some((context_menu, position, subscription));
1720
1721        cx.notify();
1722    }
1723
1724    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1725        if self.take_editing_state(cx) {
1726            cx.focus_view(&self.filter_editor);
1727        } else {
1728            self.filter_editor.update(cx, |editor, cx| {
1729                if editor.buffer().read(cx).len(cx) > 0 {
1730                    editor.set_text("", cx);
1731                }
1732            });
1733        }
1734
1735        self.update_entries(false, cx);
1736    }
1737
1738    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1739        let ix = self.selection.map_or(0, |ix| ix + 1);
1740        if ix < self.entries.len() {
1741            self.selection = Some(ix);
1742        }
1743
1744        if let Some(ix) = self.selection {
1745            self.scroll_handle.scroll_to_item(ix)
1746        }
1747        cx.notify();
1748    }
1749
1750    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1751        let ix = self.selection.take().unwrap_or(0);
1752        if ix > 0 {
1753            self.selection = Some(ix - 1);
1754        }
1755
1756        if let Some(ix) = self.selection {
1757            self.scroll_handle.scroll_to_item(ix)
1758        }
1759        cx.notify();
1760    }
1761
1762    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1763        if self.confirm_channel_edit(cx) {
1764            return;
1765        }
1766
1767        if let Some(selection) = self.selection {
1768            if let Some(entry) = self.entries.get(selection) {
1769                match entry {
1770                    ListEntry::Header(section) => match section {
1771                        Section::ActiveCall => Self::leave_call(cx),
1772                        Section::Channels => self.new_root_channel(cx),
1773                        Section::Contacts => self.toggle_contact_finder(cx),
1774                        Section::ContactRequests
1775                        | Section::Online
1776                        | Section::Offline
1777                        | Section::ChannelInvites => {
1778                            self.toggle_section_expanded(*section, cx);
1779                        }
1780                    },
1781                    ListEntry::Contact { contact, calling } => {
1782                        if contact.online && !contact.busy && !calling {
1783                            self.call(contact.user.id, cx);
1784                        }
1785                    }
1786                    // ListEntry::ParticipantProject {
1787                    //     project_id,
1788                    //     host_user_id,
1789                    //     ..
1790                    // } => {
1791                    //     if let Some(workspace) = self.workspace.upgrade(cx) {
1792                    //         let app_state = workspace.read(cx).app_state().clone();
1793                    //         workspace::join_remote_project(
1794                    //             *project_id,
1795                    //             *host_user_id,
1796                    //             app_state,
1797                    //             cx,
1798                    //         )
1799                    //         .detach_and_log_err(cx);
1800                    //     }
1801                    // }
1802                    // ListEntry::ParticipantScreen { peer_id, .. } => {
1803                    //     let Some(peer_id) = peer_id else {
1804                    //         return;
1805                    //     };
1806                    //     if let Some(workspace) = self.workspace.upgrade(cx) {
1807                    //         workspace.update(cx, |workspace, cx| {
1808                    //             workspace.open_shared_screen(*peer_id, cx)
1809                    //         });
1810                    //     }
1811                    // }
1812                    ListEntry::Channel { channel, .. } => {
1813                        let is_active = maybe!({
1814                            let call_channel = ActiveCall::global(cx)
1815                                .read(cx)
1816                                .room()?
1817                                .read(cx)
1818                                .channel_id()?;
1819
1820                            Some(call_channel == channel.id)
1821                        })
1822                        .unwrap_or(false);
1823                        if is_active {
1824                            self.open_channel_notes(channel.id, cx)
1825                        } else {
1826                            self.join_channel(channel.id, cx)
1827                        }
1828                    }
1829                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
1830                    _ => {}
1831                }
1832            }
1833        }
1834    }
1835
1836    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
1837        if self.channel_editing_state.is_some() {
1838            self.channel_name_editor.update(cx, |editor, cx| {
1839                editor.insert(" ", cx);
1840            });
1841        }
1842    }
1843
1844    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
1845        if let Some(editing_state) = &mut self.channel_editing_state {
1846            match editing_state {
1847                ChannelEditingState::Create {
1848                    location,
1849                    pending_name,
1850                    ..
1851                } => {
1852                    if pending_name.is_some() {
1853                        return false;
1854                    }
1855                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1856
1857                    *pending_name = Some(channel_name.clone());
1858
1859                    self.channel_store
1860                        .update(cx, |channel_store, cx| {
1861                            channel_store.create_channel(&channel_name, *location, cx)
1862                        })
1863                        .detach();
1864                    cx.notify();
1865                }
1866                ChannelEditingState::Rename {
1867                    location,
1868                    pending_name,
1869                } => {
1870                    if pending_name.is_some() {
1871                        return false;
1872                    }
1873                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1874                    *pending_name = Some(channel_name.clone());
1875
1876                    self.channel_store
1877                        .update(cx, |channel_store, cx| {
1878                            channel_store.rename(*location, &channel_name, cx)
1879                        })
1880                        .detach();
1881                    cx.notify();
1882                }
1883            }
1884            cx.focus_self();
1885            true
1886        } else {
1887            false
1888        }
1889    }
1890
1891    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1892        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1893            self.collapsed_sections.remove(ix);
1894        } else {
1895            self.collapsed_sections.push(section);
1896        }
1897        self.update_entries(false, cx);
1898    }
1899
1900    fn collapse_selected_channel(
1901        &mut self,
1902        _: &CollapseSelectedChannel,
1903        cx: &mut ViewContext<Self>,
1904    ) {
1905        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1906            return;
1907        };
1908
1909        if self.is_channel_collapsed(channel_id) {
1910            return;
1911        }
1912
1913        self.toggle_channel_collapsed(channel_id, cx);
1914    }
1915
1916    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
1917        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1918            return;
1919        };
1920
1921        if !self.is_channel_collapsed(id) {
1922            return;
1923        }
1924
1925        self.toggle_channel_collapsed(id, cx)
1926    }
1927
1928    //     fn toggle_channel_collapsed_action(
1929    //         &mut self,
1930    //         action: &ToggleCollapse,
1931    //         cx: &mut ViewContext<Self>,
1932    //     ) {
1933    //         self.toggle_channel_collapsed(action.location, cx);
1934    //     }
1935
1936    fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1937        match self.collapsed_channels.binary_search(&channel_id) {
1938            Ok(ix) => {
1939                self.collapsed_channels.remove(ix);
1940            }
1941            Err(ix) => {
1942                self.collapsed_channels.insert(ix, channel_id);
1943            }
1944        };
1945        self.serialize(cx);
1946        self.update_entries(true, cx);
1947        cx.notify();
1948        cx.focus_self();
1949    }
1950
1951    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1952        self.collapsed_channels.binary_search(&channel_id).is_ok()
1953    }
1954
1955    fn leave_call(cx: &mut ViewContext<Self>) {
1956        ActiveCall::global(cx)
1957            .update(cx, |call, cx| call.hang_up(cx))
1958            .detach_and_log_err(cx);
1959    }
1960
1961    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1962        if let Some(workspace) = self.workspace.upgrade() {
1963            workspace.update(cx, |workspace, cx| {
1964                workspace.toggle_modal(cx, |cx| {
1965                    let mut finder = ContactFinder::new(self.user_store.clone(), cx);
1966                    finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1967                    finder
1968                });
1969            });
1970        }
1971    }
1972
1973    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
1974        self.channel_editing_state = Some(ChannelEditingState::Create {
1975            location: None,
1976            pending_name: None,
1977        });
1978        self.update_entries(false, cx);
1979        self.select_channel_editor();
1980        cx.focus_view(&self.channel_name_editor);
1981        cx.notify();
1982    }
1983
1984    fn select_channel_editor(&mut self) {
1985        self.selection = self.entries.iter().position(|entry| match entry {
1986            ListEntry::ChannelEditor { .. } => true,
1987            _ => false,
1988        });
1989    }
1990
1991    fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1992        self.collapsed_channels
1993            .retain(|channel| *channel != channel_id);
1994        self.channel_editing_state = Some(ChannelEditingState::Create {
1995            location: Some(channel_id),
1996            pending_name: None,
1997        });
1998        self.update_entries(false, cx);
1999        self.select_channel_editor();
2000        cx.focus_view(&self.channel_name_editor);
2001        cx.notify();
2002    }
2003
2004    fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2005        self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
2006    }
2007
2008    fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2009        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
2010    }
2011
2012    fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
2013        if let Some(channel) = self.selected_channel() {
2014            self.remove_channel(channel.id, cx)
2015        }
2016    }
2017
2018    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
2019        if let Some(channel) = self.selected_channel() {
2020            self.rename_channel(channel.id, cx);
2021        }
2022    }
2023
2024    fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2025        let channel_store = self.channel_store.read(cx);
2026        if !channel_store.is_channel_admin(channel_id) {
2027            return;
2028        }
2029        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
2030            self.channel_editing_state = Some(ChannelEditingState::Rename {
2031                location: channel_id,
2032                pending_name: None,
2033            });
2034            self.channel_name_editor.update(cx, |editor, cx| {
2035                editor.set_text(channel.name.clone(), cx);
2036                editor.select_all(&Default::default(), cx);
2037            });
2038            cx.focus_view(&self.channel_name_editor);
2039            self.update_entries(false, cx);
2040            self.select_channel_editor();
2041        }
2042    }
2043
2044    fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2045        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
2046    }
2047
2048    fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2049        if let Some(channel) = self.selected_channel() {
2050            self.channel_clipboard = Some(ChannelMoveClipboard {
2051                channel_id: channel.id,
2052            })
2053        }
2054    }
2055
2056    fn move_channel_on_clipboard(
2057        &mut self,
2058        to_channel_id: ChannelId,
2059        cx: &mut ViewContext<CollabPanel>,
2060    ) {
2061        if let Some(clipboard) = self.channel_clipboard.take() {
2062            self.channel_store.update(cx, |channel_store, cx| {
2063                channel_store
2064                    .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
2065                    .detach_and_log_err(cx)
2066            })
2067        }
2068    }
2069
2070    fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2071        if let Some(workspace) = self.workspace.upgrade() {
2072            todo!();
2073            // ChannelView::open(action.channel_id, workspace, cx).detach();
2074        }
2075    }
2076
2077    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
2078        let Some(channel) = self.selected_channel() else {
2079            return;
2080        };
2081        let Some(bounds) = self
2082            .selection
2083            .and_then(|ix| self.scroll_handle.bounds_for_item(ix))
2084        else {
2085            return;
2086        };
2087
2088        self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx);
2089        cx.stop_propagation();
2090    }
2091
2092    fn selected_channel(&self) -> Option<&Arc<Channel>> {
2093        self.selection
2094            .and_then(|ix| self.entries.get(ix))
2095            .and_then(|entry| match entry {
2096                ListEntry::Channel { channel, .. } => Some(channel),
2097                _ => None,
2098            })
2099    }
2100
2101    fn show_channel_modal(
2102        &mut self,
2103        channel_id: ChannelId,
2104        mode: channel_modal::Mode,
2105        cx: &mut ViewContext<Self>,
2106    ) {
2107        let workspace = self.workspace.clone();
2108        let user_store = self.user_store.clone();
2109        let channel_store = self.channel_store.clone();
2110        let members = self.channel_store.update(cx, |channel_store, cx| {
2111            channel_store.get_channel_member_details(channel_id, cx)
2112        });
2113
2114        cx.spawn(|_, mut cx| async move {
2115            let members = members.await?;
2116            workspace.update(&mut cx, |workspace, cx| {
2117                workspace.toggle_modal(cx, |cx| {
2118                    ChannelModal::new(
2119                        user_store.clone(),
2120                        channel_store.clone(),
2121                        channel_id,
2122                        mode,
2123                        members,
2124                        cx,
2125                    )
2126                });
2127            })
2128        })
2129        .detach();
2130    }
2131
2132    //     fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
2133    //         self.remove_channel(action.channel_id, cx)
2134    //     }
2135
2136    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2137        let channel_store = self.channel_store.clone();
2138        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2139            let prompt_message = format!(
2140                "Are you sure you want to remove the channel \"{}\"?",
2141                channel.name
2142            );
2143            let mut answer =
2144                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2145            let window = cx.window();
2146            cx.spawn(|this, mut cx| async move {
2147                if answer.await? == 0 {
2148                    channel_store
2149                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
2150                        .await
2151                        .notify_async_err(&mut cx);
2152                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
2153                }
2154                anyhow::Ok(())
2155            })
2156            .detach();
2157        }
2158    }
2159
2160    //     // Should move to the filter editor if clicking on it
2161    //     // Should move selection to the channel editor if activating it
2162
2163    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2164        let user_store = self.user_store.clone();
2165        let prompt_message = format!(
2166            "Are you sure you want to remove \"{}\" from your contacts?",
2167            github_login
2168        );
2169        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2170        let window = cx.window();
2171        cx.spawn(|_, mut cx| async move {
2172            if answer.await? == 0 {
2173                user_store
2174                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
2175                    .await
2176                    .notify_async_err(&mut cx);
2177            }
2178            anyhow::Ok(())
2179        })
2180        .detach_and_log_err(cx);
2181    }
2182
2183    fn respond_to_contact_request(
2184        &mut self,
2185        user_id: u64,
2186        accept: bool,
2187        cx: &mut ViewContext<Self>,
2188    ) {
2189        self.user_store
2190            .update(cx, |store, cx| {
2191                store.respond_to_contact_request(user_id, accept, cx)
2192            })
2193            .detach_and_log_err(cx);
2194    }
2195
2196    //     fn respond_to_channel_invite(
2197    //         &mut self,
2198    //         channel_id: u64,
2199    //         accept: bool,
2200    //         cx: &mut ViewContext<Self>,
2201    //     ) {
2202    //         self.channel_store
2203    //             .update(cx, |store, cx| {
2204    //                 store.respond_to_channel_invite(channel_id, accept, cx)
2205    //             })
2206    //             .detach();
2207    //     }
2208
2209    fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext<Self>) {
2210        ActiveCall::global(cx)
2211            .update(cx, |call, cx| {
2212                call.invite(recipient_user_id, Some(self.project.clone()), cx)
2213            })
2214            .detach_and_log_err(cx);
2215    }
2216
2217    fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
2218        let Some(workspace) = self.workspace.upgrade() else {
2219            return;
2220        };
2221        let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
2222            return;
2223        };
2224        workspace::join_channel(
2225            channel_id,
2226            workspace.read(cx).app_state().clone(),
2227            Some(handle),
2228            cx,
2229        )
2230        .detach_and_log_err(cx)
2231    }
2232
2233    fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2234        let Some(workspace) = self.workspace.upgrade() else {
2235            return;
2236        };
2237        cx.window_context().defer(move |cx| {
2238            workspace.update(cx, |workspace, cx| {
2239                todo!();
2240                // if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
2241                //     panel.update(cx, |panel, cx| {
2242                //         panel
2243                //             .select_channel(channel_id, None, cx)
2244                //             .detach_and_log_err(cx);
2245                //     });
2246                // }
2247            });
2248        });
2249    }
2250
2251    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2252        let channel_store = self.channel_store.read(cx);
2253        let Some(channel) = channel_store.channel_for_id(channel_id) else {
2254            return;
2255        };
2256        let item = ClipboardItem::new(channel.link());
2257        cx.write_to_clipboard(item)
2258    }
2259
2260    fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
2261        v_stack().child(
2262            Button::new("sign_in", "Sign in to collaborate").on_click(cx.listener(
2263                |this, _, cx| {
2264                    let client = this.client.clone();
2265                    cx.spawn(|_, mut cx| async move {
2266                        client
2267                            .authenticate_and_connect(true, &cx)
2268                            .await
2269                            .notify_async_err(&mut cx);
2270                    })
2271                    .detach()
2272                },
2273            )),
2274        )
2275    }
2276
2277    fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
2278        v_stack()
2279            .size_full()
2280            .child(
2281                div()
2282                    .p_2()
2283                    .child(div().rounded(px(2.0)).child(self.filter_editor.clone())),
2284            )
2285            .child(
2286                v_stack()
2287                    .size_full()
2288                    .id("scroll")
2289                    .overflow_y_scroll()
2290                    .track_scroll(&self.scroll_handle)
2291                    .children(
2292                        self.entries
2293                            .clone()
2294                            .into_iter()
2295                            .enumerate()
2296                            .map(|(ix, entry)| {
2297                                let is_selected = self.selection == Some(ix);
2298                                match entry {
2299                                    ListEntry::Header(section) => {
2300                                        let is_collapsed =
2301                                            self.collapsed_sections.contains(&section);
2302                                        self.render_header(section, is_selected, is_collapsed, cx)
2303                                            .into_any_element()
2304                                    }
2305                                    ListEntry::Contact { contact, calling } => self
2306                                        .render_contact(&*contact, calling, is_selected, cx)
2307                                        .into_any_element(),
2308                                    ListEntry::ContactPlaceholder => self
2309                                        .render_contact_placeholder(is_selected, cx)
2310                                        .into_any_element(),
2311                                    ListEntry::IncomingRequest(user) => self
2312                                        .render_contact_request(user, true, is_selected, cx)
2313                                        .into_any_element(),
2314                                    ListEntry::OutgoingRequest(user) => self
2315                                        .render_contact_request(user, false, is_selected, cx)
2316                                        .into_any_element(),
2317                                    ListEntry::Channel {
2318                                        channel,
2319                                        depth,
2320                                        has_children,
2321                                    } => self
2322                                        .render_channel(
2323                                            &*channel,
2324                                            depth,
2325                                            has_children,
2326                                            is_selected,
2327                                            ix,
2328                                            cx,
2329                                        )
2330                                        .into_any_element(),
2331                                    ListEntry::ChannelEditor { depth } => {
2332                                        self.render_channel_editor(depth, cx).into_any_element()
2333                                    }
2334                                    ListEntry::CallParticipant {
2335                                        user,
2336                                        peer_id,
2337                                        is_pending,
2338                                    } => self
2339                                        .render_call_participant(user, peer_id, is_pending, cx)
2340                                        .into_any_element(),
2341                                    ListEntry::ParticipantProject {
2342                                        project_id,
2343                                        worktree_root_names,
2344                                        host_user_id,
2345                                        is_last,
2346                                    } => self
2347                                        .render_participant_project(
2348                                            project_id,
2349                                            &worktree_root_names,
2350                                            host_user_id,
2351                                            is_last,
2352                                            cx,
2353                                        )
2354                                        .into_any_element(),
2355                                    ListEntry::ParticipantScreen { peer_id, is_last } => self
2356                                        .render_participant_screen(peer_id, is_last, cx)
2357                                        .into_any_element(),
2358                                    ListEntry::ChannelNotes { channel_id } => {
2359                                        self.render_channel_notes(channel_id, cx).into_any_element()
2360                                    }
2361                                    ListEntry::ChannelChat { channel_id } => {
2362                                        self.render_channel_chat(channel_id, cx).into_any_element()
2363                                    }
2364                                }
2365                            }),
2366                    ),
2367            )
2368    }
2369
2370    fn render_header(
2371        &mut self,
2372        section: Section,
2373        is_selected: bool,
2374        is_collapsed: bool,
2375        cx: &ViewContext<Self>,
2376    ) -> impl IntoElement {
2377        let mut channel_link = None;
2378        let mut channel_tooltip_text = None;
2379        let mut channel_icon = None;
2380        // let mut is_dragged_over = false;
2381
2382        let text = match section {
2383            Section::ActiveCall => {
2384                let channel_name = maybe!({
2385                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2386
2387                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2388
2389                    channel_link = Some(channel.link());
2390                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2391                        proto::ChannelVisibility::Public => {
2392                            (Some("icons/public.svg"), Some("Copy public channel link."))
2393                        }
2394                        proto::ChannelVisibility::Members => {
2395                            (Some("icons/hash.svg"), Some("Copy private channel link."))
2396                        }
2397                    };
2398
2399                    Some(channel.name.as_str())
2400                });
2401
2402                if let Some(name) = channel_name {
2403                    SharedString::from(format!("{}", name))
2404                } else {
2405                    SharedString::from("Current Call")
2406                }
2407            }
2408            Section::ContactRequests => SharedString::from("Requests"),
2409            Section::Contacts => SharedString::from("Contacts"),
2410            Section::Channels => SharedString::from("Channels"),
2411            Section::ChannelInvites => SharedString::from("Invites"),
2412            Section::Online => SharedString::from("Online"),
2413            Section::Offline => SharedString::from("Offline"),
2414        };
2415
2416        let button = match section {
2417            Section::ActiveCall => channel_link.map(|channel_link| {
2418                let channel_link_copy = channel_link.clone();
2419                IconButton::new("channel-link", Icon::Check)
2420                    .on_click(move |_, cx| {
2421                        let item = ClipboardItem::new(channel_link_copy.clone());
2422                        cx.write_to_clipboard(item)
2423                    })
2424                    .tooltip(|cx| Tooltip::text("Copy channel link", cx))
2425            }),
2426            Section::Contacts => Some(
2427                IconButton::new("add-contact", Icon::Plus)
2428                    .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2429                    .tooltip(|cx| Tooltip::text("Search for new contact", cx)),
2430            ),
2431            Section::Channels => Some(
2432                IconButton::new("add-channel", Icon::Plus)
2433                    .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
2434                    .tooltip(|cx| Tooltip::text("Create a channel", cx)),
2435            ),
2436            _ => None,
2437        };
2438
2439        let can_collapse = match section {
2440            Section::ActiveCall | Section::Channels | Section::Contacts => false,
2441            Section::ChannelInvites
2442            | Section::ContactRequests
2443            | Section::Online
2444            | Section::Offline => true,
2445        };
2446
2447        h_stack()
2448            .w_full()
2449            .map(|el| {
2450                if can_collapse {
2451                    el.child(
2452                        ListItem::new(text.clone())
2453                            .child(div().w_full().child(Label::new(text)))
2454                            .selected(is_selected)
2455                            .toggle(Some(!is_collapsed))
2456                            .on_click(cx.listener(move |this, _, cx| {
2457                                this.toggle_section_expanded(section, cx)
2458                            })),
2459                    )
2460                } else {
2461                    el.child(
2462                        ListHeader::new(text)
2463                            .when_some(button, |el, button| el.meta(button))
2464                            .selected(is_selected),
2465                    )
2466                }
2467            })
2468            .when(section == Section::Channels, |el| {
2469                el.drag_over::<DraggedChannelView>(|style| {
2470                    style.bg(cx.theme().colors().ghost_element_hover)
2471                })
2472                .on_drop(cx.listener(
2473                    move |this, view: &View<DraggedChannelView>, cx| {
2474                        this.channel_store
2475                            .update(cx, |channel_store, cx| {
2476                                channel_store.move_channel(view.read(cx).channel.id, None, cx)
2477                            })
2478                            .detach_and_log_err(cx)
2479                    },
2480                ))
2481            })
2482    }
2483
2484    fn render_contact(
2485        &mut self,
2486        contact: &Contact,
2487        calling: bool,
2488        is_selected: bool,
2489        cx: &mut ViewContext<Self>,
2490    ) -> impl IntoElement {
2491        enum ContactTooltip {}
2492
2493        let online = contact.online;
2494        let busy = contact.busy || calling;
2495        let user_id = contact.user.id;
2496        let github_login = SharedString::from(contact.user.github_login.clone());
2497        let mut item = ListItem::new(github_login.clone())
2498            .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2499            .child(
2500                h_stack()
2501                    .w_full()
2502                    .justify_between()
2503                    .child(Label::new(github_login.clone()))
2504                    .when(calling, |el| {
2505                        el.child(Label::new("Calling").color(Color::Muted))
2506                    })
2507                    .when(!calling, |el| {
2508                        el.child(
2509                            div()
2510                                .id("remove_contact")
2511                                .invisible()
2512                                .group_hover("", |style| style.visible())
2513                                .child(
2514                                    IconButton::new("remove_contact", Icon::Close)
2515                                        .icon_color(Color::Muted)
2516                                        .tooltip(|cx| Tooltip::text("Remove Contact", cx))
2517                                        .on_click(cx.listener({
2518                                            let github_login = github_login.clone();
2519                                            move |this, _, cx| {
2520                                                this.remove_contact(user_id, &github_login, cx);
2521                                            }
2522                                        })),
2523                                ),
2524                        )
2525                    }),
2526            )
2527            .left_child(
2528                // todo!() handle contacts with no avatar
2529                Avatar::data(contact.user.avatar.clone().unwrap())
2530                    .availability_indicator(if online { Some(!busy) } else { None }),
2531            )
2532            .when(online && !busy, |el| {
2533                el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2534            });
2535
2536        div()
2537            .id(github_login.clone())
2538            .group("")
2539            .child(item)
2540            .tooltip(move |cx| {
2541                let text = if !online {
2542                    format!(" {} is offline", &github_login)
2543                } else if busy {
2544                    format!(" {} is on a call", &github_login)
2545                } else {
2546                    let room = ActiveCall::global(cx).read(cx).room();
2547                    if room.is_some() {
2548                        format!("Invite {} to join call", &github_login)
2549                    } else {
2550                        format!("Call {}", &github_login)
2551                    }
2552                };
2553                Tooltip::text(text, cx)
2554            })
2555    }
2556
2557    fn render_contact_request(
2558        &mut self,
2559        user: Arc<User>,
2560        is_incoming: bool,
2561        is_selected: bool,
2562        cx: &mut ViewContext<Self>,
2563    ) -> impl IntoElement {
2564        let github_login = SharedString::from(user.github_login.clone());
2565        let user_id = user.id;
2566        let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2567        let color = if is_contact_request_pending {
2568            Color::Muted
2569        } else {
2570            Color::Default
2571        };
2572
2573        let controls = if is_incoming {
2574            vec![
2575                IconButton::new("remove_contact", Icon::Close)
2576                    .on_click(cx.listener(move |this, _, cx| {
2577                        this.respond_to_contact_request(user_id, false, cx);
2578                    }))
2579                    .icon_color(color)
2580                    .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2581                IconButton::new("remove_contact", Icon::Check)
2582                    .on_click(cx.listener(move |this, _, cx| {
2583                        this.respond_to_contact_request(user_id, true, cx);
2584                    }))
2585                    .icon_color(color)
2586                    .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2587            ]
2588        } else {
2589            let github_login = github_login.clone();
2590            vec![IconButton::new("remove_contact", Icon::Close)
2591                .on_click(cx.listener(move |this, _, cx| {
2592                    this.remove_contact(user_id, &github_login, cx);
2593                }))
2594                .icon_color(color)
2595                .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2596        };
2597
2598        ListItem::new(github_login.clone())
2599            .child(
2600                h_stack()
2601                    .w_full()
2602                    .justify_between()
2603                    .child(Label::new(github_login.clone()))
2604                    .child(h_stack().children(controls)),
2605            )
2606            .when_some(user.avatar.clone(), |el, avatar| el.left_avatar(avatar))
2607    }
2608
2609    fn render_contact_placeholder(
2610        &self,
2611        is_selected: bool,
2612        cx: &mut ViewContext<Self>,
2613    ) -> impl IntoElement {
2614        ListItem::new("contact-placeholder")
2615            .child(IconElement::new(Icon::Plus))
2616            .child(Label::new("Add a Contact"))
2617            .selected(is_selected)
2618            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2619    }
2620
2621    fn render_channel(
2622        &self,
2623        channel: &Channel,
2624        depth: usize,
2625        has_children: bool,
2626        is_selected: bool,
2627        ix: usize,
2628        cx: &mut ViewContext<Self>,
2629    ) -> impl IntoElement {
2630        let channel_id = channel.id;
2631
2632        let is_active = maybe!({
2633            let call_channel = ActiveCall::global(cx)
2634                .read(cx)
2635                .room()?
2636                .read(cx)
2637                .channel_id()?;
2638            Some(call_channel == channel_id)
2639        })
2640        .unwrap_or(false);
2641        let is_public = self
2642            .channel_store
2643            .read(cx)
2644            .channel_for_id(channel_id)
2645            .map(|channel| channel.visibility)
2646            == Some(proto::ChannelVisibility::Public);
2647        let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2648        let disclosed =
2649            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2650
2651        let has_messages_notification = channel.unseen_message_id.is_some();
2652        let has_notes_notification = channel.unseen_note_version.is_some();
2653
2654        const FACEPILE_LIMIT: usize = 3;
2655        let participants = self.channel_store.read(cx).channel_participants(channel_id);
2656
2657        let face_pile = if !participants.is_empty() {
2658            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2659            let user = &participants[0];
2660
2661            let result = FacePile {
2662                faces: participants
2663                    .iter()
2664                    .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element()))
2665                    .take(FACEPILE_LIMIT)
2666                    .chain(if extra_count > 0 {
2667                        // todo!() @nate - this label looks wrong.
2668                        Some(Label::new(format!("+{}", extra_count)).into_any_element())
2669                    } else {
2670                        None
2671                    })
2672                    .collect::<Vec<_>>(),
2673            };
2674
2675            Some(result)
2676        } else {
2677            None
2678        };
2679
2680        let width = self.width.unwrap_or(px(240.));
2681
2682        div()
2683            .id(channel_id as usize)
2684            .group("")
2685            .on_drag({
2686                let channel = channel.clone();
2687                move |cx| {
2688                    let channel = channel.clone();
2689                    cx.build_view({ |cx| DraggedChannelView { channel, width } })
2690                }
2691            })
2692            .drag_over::<DraggedChannelView>(|style| {
2693                style.bg(cx.theme().colors().ghost_element_hover)
2694            })
2695            .on_drop(
2696                cx.listener(move |this, view: &View<DraggedChannelView>, cx| {
2697                    this.channel_store
2698                        .update(cx, |channel_store, cx| {
2699                            channel_store.move_channel(
2700                                view.read(cx).channel.id,
2701                                Some(channel_id),
2702                                cx,
2703                            )
2704                        })
2705                        .detach_and_log_err(cx)
2706                }),
2707            )
2708            .child(
2709                ListItem::new(channel_id as usize)
2710                    .indent_level(depth)
2711                    .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to  step over the disclosure toggle
2712                    .left_icon(if is_public { Icon::Public } else { Icon::Hash })
2713                    .selected(is_selected || is_active)
2714                    .child(
2715                        h_stack()
2716                            .w_full()
2717                            .justify_between()
2718                            .child(
2719                                h_stack()
2720                                    .id(channel_id as usize)
2721                                    .child(Label::new(channel.name.clone()))
2722                                    .children(face_pile.map(|face_pile| face_pile.render(cx))),
2723                            )
2724                            .child(
2725                                h_stack()
2726                                    .child(
2727                                        div()
2728                                            .id("channel_chat")
2729                                            .when(!has_messages_notification, |el| el.invisible())
2730                                            .group_hover("", |style| style.visible())
2731                                            .child(
2732                                                IconButton::new(
2733                                                    "channel_chat",
2734                                                    Icon::MessageBubbles,
2735                                                )
2736                                                .icon_color(if has_messages_notification {
2737                                                    Color::Default
2738                                                } else {
2739                                                    Color::Muted
2740                                                }),
2741                                            )
2742                                            .tooltip(|cx| Tooltip::text("Open channel chat", cx)),
2743                                    )
2744                                    .child(
2745                                        div()
2746                                            .id("channel_notes")
2747                                            .when(!has_notes_notification, |el| el.invisible())
2748                                            .group_hover("", |style| style.visible())
2749                                            .child(
2750                                                IconButton::new("channel_notes", Icon::File)
2751                                                    .icon_color(if has_notes_notification {
2752                                                        Color::Default
2753                                                    } else {
2754                                                        Color::Muted
2755                                                    })
2756                                                    .tooltip(|cx| {
2757                                                        Tooltip::text("Open channel notes", cx)
2758                                                    }),
2759                                            ),
2760                                    ),
2761                            ),
2762                    )
2763                    .toggle(disclosed)
2764                    .on_toggle(
2765                        cx.listener(move |this, _, cx| {
2766                            this.toggle_channel_collapsed(channel_id, cx)
2767                        }),
2768                    )
2769                    .on_click(cx.listener(move |this, _, cx| {
2770                        if this.drag_target_channel == ChannelDragTarget::None {
2771                            if is_active {
2772                                this.open_channel_notes(channel_id, cx)
2773                            } else {
2774                                this.join_channel(channel_id, cx)
2775                            }
2776                        }
2777                    }))
2778                    .on_secondary_mouse_down(cx.listener(
2779                        move |this, event: &MouseDownEvent, cx| {
2780                            this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2781                        },
2782                    )),
2783            )
2784            .tooltip(|cx| Tooltip::text("Join channel", cx))
2785
2786        // let channel_id = channel.id;
2787        // let collab_theme = &theme.collab_panel;
2788        // let is_public = self
2789        //     .channel_store
2790        //     .read(cx)
2791        //     .channel_for_id(channel_id)
2792        //     .map(|channel| channel.visibility)
2793        //     == Some(proto::ChannelVisibility::Public);
2794        // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2795        // let disclosed =
2796        //     has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2797
2798        // enum ChannelCall {}
2799        // enum ChannelNote {}
2800        // enum NotesTooltip {}
2801        // enum ChatTooltip {}
2802        // enum ChannelTooltip {}
2803
2804        // let mut is_dragged_over = false;
2805        // if cx
2806        //     .global::<DragAndDrop<Workspace>>()
2807        //     .currently_dragged::<Channel>(cx.window())
2808        //     .is_some()
2809        //     && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
2810        // {
2811        //     is_dragged_over = true;
2812        // }
2813
2814        // let has_messages_notification = channel.unseen_message_id.is_some();
2815
2816        // MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
2817        //     let row_hovered = state.hovered();
2818
2819        //     let mut select_state = |interactive: &Interactive<ContainerStyle>| {
2820        //         if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
2821        //             interactive.clicked.as_ref().unwrap().clone()
2822        //         } else if state.hovered() || other_selected {
2823        //             interactive
2824        //                 .hovered
2825        //                 .as_ref()
2826        //                 .unwrap_or(&interactive.default)
2827        //                 .clone()
2828        //         } else {
2829        //             interactive.default.clone()
2830        //         }
2831        //     };
2832
2833        //     Flex::<Self>::row()
2834        //         .with_child(
2835        //             Svg::new(if is_public {
2836        //                 "icons/public.svg"
2837        //             } else {
2838        //                 "icons/hash.svg"
2839        //             })
2840        //             .with_color(collab_theme.channel_hash.color)
2841        //             .constrained()
2842        //             .with_width(collab_theme.channel_hash.width)
2843        //             .aligned()
2844        //             .left(),
2845        //         )
2846        //         .with_child({
2847        //             let style = collab_theme.channel_name.inactive_state();
2848        //             Flex::row()
2849        //                 .with_child(
2850        //                     Label::new(channel.name.clone(), style.text.clone())
2851        //                         .contained()
2852        //                         .with_style(style.container)
2853        //                         .aligned()
2854        //                         .left()
2855        //                         .with_tooltip::<ChannelTooltip>(
2856        //                             ix,
2857        //                             "Join channel",
2858        //                             None,
2859        //                             theme.tooltip.clone(),
2860        //                             cx,
2861        //                         ),
2862        //                 )
2863        //                 .with_children({
2864        //                     let participants =
2865        //                         self.channel_store.read(cx).channel_participants(channel_id);
2866
2867        //                     if !participants.is_empty() {
2868        //                         let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2869
2870        //                         let result = FacePile::new(collab_theme.face_overlap)
2871        //                             .with_children(
2872        //                                 participants
2873        //                                     .iter()
2874        //                                     .filter_map(|user| {
2875        //                                         Some(
2876        //                                             Image::from_data(user.avatar.clone()?)
2877        //                                                 .with_style(collab_theme.channel_avatar),
2878        //                                         )
2879        //                                     })
2880        //                                     .take(FACEPILE_LIMIT),
2881        //                             )
2882        //                             .with_children((extra_count > 0).then(|| {
2883        //                                 Label::new(
2884        //                                     format!("+{}", extra_count),
2885        //                                     collab_theme.extra_participant_label.text.clone(),
2886        //                                 )
2887        //                                 .contained()
2888        //                                 .with_style(collab_theme.extra_participant_label.container)
2889        //                             }));
2890
2891        //                         Some(result)
2892        //                     } else {
2893        //                         None
2894        //                     }
2895        //                 })
2896        //                 .with_spacing(8.)
2897        //                 .align_children_center()
2898        //                 .flex(1., true)
2899        //         })
2900        //         .with_child(
2901        //             MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
2902        //                 let container_style = collab_theme
2903        //                     .disclosure
2904        //                     .button
2905        //                     .style_for(mouse_state)
2906        //                     .container;
2907
2908        //                 if channel.unseen_message_id.is_some() {
2909        //                     Svg::new("icons/conversations.svg")
2910        //                         .with_color(collab_theme.channel_note_active_color)
2911        //                         .constrained()
2912        //                         .with_width(collab_theme.channel_hash.width)
2913        //                         .contained()
2914        //                         .with_style(container_style)
2915        //                         .with_uniform_padding(4.)
2916        //                         .into_any()
2917        //                 } else if row_hovered {
2918        //                     Svg::new("icons/conversations.svg")
2919        //                         .with_color(collab_theme.channel_hash.color)
2920        //                         .constrained()
2921        //                         .with_width(collab_theme.channel_hash.width)
2922        //                         .contained()
2923        //                         .with_style(container_style)
2924        //                         .with_uniform_padding(4.)
2925        //                         .into_any()
2926        //                 } else {
2927        //                     Empty::new().into_any()
2928        //                 }
2929        //             })
2930        //             .on_click(MouseButton::Left, move |_, this, cx| {
2931        //                 this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
2932        //             })
2933        //             .with_tooltip::<ChatTooltip>(
2934        //                 ix,
2935        //                 "Open channel chat",
2936        //                 None,
2937        //                 theme.tooltip.clone(),
2938        //                 cx,
2939        //             )
2940        //             .contained()
2941        //             .with_margin_right(4.),
2942        //         )
2943        //         .with_child(
2944        //             MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
2945        //                 let container_style = collab_theme
2946        //                     .disclosure
2947        //                     .button
2948        //                     .style_for(mouse_state)
2949        //                     .container;
2950        //                 if row_hovered || channel.unseen_note_version.is_some() {
2951        //                     Svg::new("icons/file.svg")
2952        //                         .with_color(if channel.unseen_note_version.is_some() {
2953        //                             collab_theme.channel_note_active_color
2954        //                         } else {
2955        //                             collab_theme.channel_hash.color
2956        //                         })
2957        //                         .constrained()
2958        //                         .with_width(collab_theme.channel_hash.width)
2959        //                         .contained()
2960        //                         .with_style(container_style)
2961        //                         .with_uniform_padding(4.)
2962        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2963        //                         .with_tooltip::<NotesTooltip>(
2964        //                             ix as usize,
2965        //                             "Open channel notes",
2966        //                             None,
2967        //                             theme.tooltip.clone(),
2968        //                             cx,
2969        //                         )
2970        //                         .into_any()
2971        //                 } else if has_messages_notification {
2972        //                     Empty::new()
2973        //                         .constrained()
2974        //                         .with_width(collab_theme.channel_hash.width)
2975        //                         .contained()
2976        //                         .with_uniform_padding(4.)
2977        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2978        //                         .into_any()
2979        //                 } else {
2980        //                     Empty::new().into_any()
2981        //                 }
2982        //             })
2983        //             .on_click(MouseButton::Left, move |_, this, cx| {
2984        //                 this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2985        //             }),
2986        //         )
2987        //         .align_children_center()
2988        //         .styleable_component()
2989        //         .disclosable(
2990        //             disclosed,
2991        //             Box::new(ToggleCollapse {
2992        //                 location: channel.id.clone(),
2993        //             }),
2994        //         )
2995        //         .with_id(ix)
2996        //         .with_style(collab_theme.disclosure.clone())
2997        //         .element()
2998        //         .constrained()
2999        //         .with_height(collab_theme.row_height)
3000        //         .contained()
3001        //         .with_style(select_state(
3002        //             collab_theme
3003        //                 .channel_row
3004        //                 .in_state(is_selected || is_active || is_dragged_over),
3005        //         ))
3006        //         .with_padding_left(
3007        //             collab_theme.channel_row.default_style().padding.left
3008        //                 + collab_theme.channel_indent * depth as f32,
3009        //         )
3010        // })
3011        // .on_click(MouseButton::Left, move |_, this, cx| {
3012        //     if this.
3013        // drag_target_channel == ChannelDragTarget::None {
3014        //         if is_active {
3015        //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
3016        //         } else {
3017        //             this.join_channel(channel_id, cx)
3018        //         }
3019        //     }
3020        // })
3021        // .on_click(MouseButton::Right, {
3022        //     let channel = channel.clone();
3023        //     move |e, this, cx| {
3024        //         this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
3025        //     }
3026        // })
3027        // .on_up(MouseButton::Left, move |_, this, cx| {
3028        //     if let Some((_, dragged_channel)) = cx
3029        //         .global::<DragAndDrop<Workspace>>()
3030        //         .currently_dragged::<Channel>(cx.window())
3031        //     {
3032        //         this.channel_store
3033        //             .update(cx, |channel_store, cx| {
3034        //                 channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
3035        //             })
3036        //             .detach_and_log_err(cx)
3037        //     }
3038        // })
3039        // .on_move({
3040        //     let channel = channel.clone();
3041        //     move |_, this, cx| {
3042        //         if let Some((_, dragged_channel)) = cx
3043        //             .global::<DragAndDrop<Workspace>>()
3044        //             .currently_dragged::<Channel>(cx.window())
3045        //         {
3046        //             if channel.id != dragged_channel.id {
3047        //                 this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
3048        //             }
3049        //             cx.notify()
3050        //         }
3051        //     }
3052        // })
3053        // .as_draggable::<_, Channel>(
3054        //     channel.clone(),
3055        //     move |_, channel, cx: &mut ViewContext<Workspace>| {
3056        //         let theme = &theme::current(cx).collab_panel;
3057
3058        //         Flex::<Workspace>::row()
3059        //             .with_child(
3060        //                 Svg::new("icons/hash.svg")
3061        //                     .with_color(theme.channel_hash.color)
3062        //                     .constrained()
3063        //                     .with_width(theme.channel_hash.width)
3064        //                     .aligned()
3065        //                     .left(),
3066        //             )
3067        //             .with_child(
3068        //                 Label::new(channel.name.clone(), theme.channel_name.text.clone())
3069        //                     .contained()
3070        //                     .with_style(theme.channel_name.container)
3071        //                     .aligned()
3072        //                     .left(),
3073        //             )
3074        //             .align_children_center()
3075        //             .contained()
3076        //             .with_background_color(
3077        //                 theme
3078        //                     .container
3079        //                     .background_color
3080        //                     .unwrap_or(gpui::color::Color::transparent_black()),
3081        //             )
3082        //             .contained()
3083        //             .with_padding_left(
3084        //                 theme.channel_row.default_style().padding.left
3085        //                     + theme.channel_indent * depth as f32,
3086        //             )
3087        //             .into_any()
3088        //     },
3089        // )
3090        // .with_cursor_style(CursorStyle::PointingHand)
3091        // .into_any()
3092    }
3093
3094    fn render_channel_editor(
3095        &mut self,
3096        depth: usize,
3097        cx: &mut ViewContext<Self>,
3098    ) -> impl IntoElement {
3099        let item = ListItem::new("channel-editor")
3100            .inset(false)
3101            .indent_level(depth)
3102            .left_icon(Icon::Hash);
3103
3104        if let Some(pending_name) = self
3105            .channel_editing_state
3106            .as_ref()
3107            .and_then(|state| state.pending_name())
3108        {
3109            item.child(Label::new(pending_name))
3110        } else {
3111            item.child(
3112                div()
3113                    .w_full()
3114                    .py_1() // todo!() @nate this is a px off at the default font size.
3115                    .child(self.channel_name_editor.clone()),
3116            )
3117        }
3118    }
3119}
3120
3121fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
3122    let text_style = cx.text_style();
3123    let rem_size = cx.rem_size();
3124    let text_system = cx.text_system();
3125    let font_id = text_system.font_id(&text_style.font()).unwrap();
3126    let font_size = text_style.font_size.to_pixels(rem_size);
3127    let line_height = text_style.line_height_in_pixels(rem_size);
3128    let cap_height = text_system.cap_height(font_id, font_size);
3129    let baseline_offset = text_system.baseline_offset(font_id, font_size, line_height);
3130    let width = cx.rem_size() * 2.5;
3131    let thickness = px(2.);
3132    let color = cx.theme().colors().text;
3133
3134    canvas(move |bounds, cx| {
3135        let start_x = bounds.left() + (bounds.size.width / 2.) - (width / 2.);
3136        let end_x = bounds.right();
3137        let start_y = bounds.top();
3138        let end_y = bounds.top() + baseline_offset - (cap_height / 2.);
3139
3140        cx.paint_quad(
3141            Bounds::from_corners(
3142                point(start_x, start_y),
3143                point(
3144                    start_x + thickness,
3145                    if is_last { end_y } else { bounds.bottom() },
3146                ),
3147            ),
3148            Default::default(),
3149            color,
3150            Default::default(),
3151            Hsla::transparent_black(),
3152        );
3153        cx.paint_quad(
3154            Bounds::from_corners(point(start_x, end_y), point(end_x, end_y + thickness)),
3155            Default::default(),
3156            color,
3157            Default::default(),
3158            Hsla::transparent_black(),
3159        );
3160    })
3161    .w(width)
3162    .h(line_height)
3163}
3164
3165impl Render for CollabPanel {
3166    type Element = Focusable<Div>;
3167
3168    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3169        v_stack()
3170            .key_context("CollabPanel")
3171            .on_action(cx.listener(CollabPanel::cancel))
3172            .on_action(cx.listener(CollabPanel::select_next))
3173            .on_action(cx.listener(CollabPanel::select_prev))
3174            .on_action(cx.listener(CollabPanel::confirm))
3175            .on_action(cx.listener(CollabPanel::insert_space))
3176            //     .on_action(cx.listener(CollabPanel::remove))
3177            .on_action(cx.listener(CollabPanel::remove_selected_channel))
3178            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
3179            //     .on_action(cx.listener(CollabPanel::new_subchannel))
3180            //     .on_action(cx.listener(CollabPanel::invite_members))
3181            //     .on_action(cx.listener(CollabPanel::manage_members))
3182            .on_action(cx.listener(CollabPanel::rename_selected_channel))
3183            //     .on_action(cx.listener(CollabPanel::rename_channel))
3184            //     .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action))
3185            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
3186            .on_action(cx.listener(CollabPanel::expand_selected_channel))
3187            //     .on_action(cx.listener(CollabPanel::open_channel_notes))
3188            //     .on_action(cx.listener(CollabPanel::join_channel_chat))
3189            //     .on_action(cx.listener(CollabPanel::copy_channel_link))
3190            .track_focus(&self.focus_handle)
3191            .size_full()
3192            .child(if self.user_store.read(cx).current_user().is_none() {
3193                self.render_signed_out(cx)
3194            } else {
3195                self.render_signed_in(cx)
3196            })
3197            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3198                overlay()
3199                    .position(*position)
3200                    .anchor(gpui::AnchorCorner::TopLeft)
3201                    .child(menu.clone())
3202            }))
3203    }
3204}
3205
3206// impl View for CollabPanel {
3207//     fn ui_name() -> &'static str {
3208//         "CollabPanel"
3209//     }
3210
3211//     fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
3212//         if !self.has_focus {
3213//             self.has_focus = true;
3214//             if !self.context_menu.is_focused(cx) {
3215//                 if let Some(editing_state) = &self.channel_editing_state {
3216//                     if editing_state.pending_name().is_none() {
3217//                         cx.focus(&self.channel_name_editor);
3218//                     } else {
3219//                         cx.focus(&self.filter_editor);
3220//                     }
3221//                 } else {
3222//                     cx.focus(&self.filter_editor);
3223//                 }
3224//             }
3225//             cx.emit(Event::Focus);
3226//         }
3227//     }
3228
3229//     fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
3230//         self.has_focus = false;
3231//     }
3232
3233//     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
3234//         let theme = &theme::current(cx).collab_panel;
3235
3236//         if self.user_store.read(cx).current_user().is_none() {
3237//             enum LogInButton {}
3238
3239//             return Flex::column()
3240//                 .with_child(
3241//                     MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
3242//                         let button = theme.log_in_button.style_for(state);
3243//                         Label::new("Sign in to collaborate", button.text.clone())
3244//                             .aligned()
3245//                             .left()
3246//                             .contained()
3247//                             .with_style(button.container)
3248//                     })
3249//                     .on_click(MouseButton::Left, |_, this, cx| {
3250//                         let client = this.client.clone();
3251//                         cx.spawn(|_, cx| async move {
3252//                             client.authenticate_and_connect(true, &cx).await.log_err();
3253//                         })
3254//                         .detach();
3255//                     })
3256//                     .with_cursor_style(CursorStyle::PointingHand),
3257//                 )
3258//                 .contained()
3259//                 .with_style(theme.container)
3260//                 .into_any();
3261//         }
3262
3263//         enum PanelFocus {}
3264//         MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
3265//             Stack::new()
3266//                 .with_child(
3267//                     Flex::column()
3268//                         .with_child(
3269//                             Flex::row().with_child(
3270//                                 ChildView::new(&self.filter_editor, cx)
3271//                                     .contained()
3272//                                     .with_style(theme.user_query_editor.container)
3273//                                     .flex(1.0, true),
3274//                             ),
3275//                         )
3276//                         .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3277//                         .contained()
3278//                         .with_style(theme.container)
3279//                         .into_any(),
3280//                 )
3281//                 .with_children(
3282//                     (!self.context_menu_on_selected)
3283//                         .then(|| ChildView::new(&self.context_menu, cx)),
3284//                 )
3285//                 .into_any()
3286//         })
3287//         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3288//         .into_any_named("collab panel")
3289//     }
3290
3291//     fn update_keymap_context(
3292//         &self,
3293//         keymap: &mut gpui::keymap_matcher::KeymapContext,
3294//         _: &AppContext,
3295//     ) {
3296//         Self::reset_to_default_keymap_context(keymap);
3297//         if self.channel_editing_state.is_some() {
3298//             keymap.add_identifier("editing");
3299//         } else {
3300//             keymap.add_identifier("not_editing");
3301//         }
3302//     }
3303// }
3304
3305impl EventEmitter<PanelEvent> for CollabPanel {}
3306
3307impl Panel for CollabPanel {
3308    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3309        CollaborationPanelSettings::get_global(cx).dock
3310    }
3311
3312    fn position_is_valid(&self, position: DockPosition) -> bool {
3313        matches!(position, DockPosition::Left | DockPosition::Right)
3314    }
3315
3316    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3317        settings::update_settings_file::<CollaborationPanelSettings>(
3318            self.fs.clone(),
3319            cx,
3320            move |settings| settings.dock = Some(position),
3321        );
3322    }
3323
3324    fn size(&self, cx: &gpui::WindowContext) -> f32 {
3325        self.width.map_or_else(
3326            || CollaborationPanelSettings::get_global(cx).default_width,
3327            |width| width.0,
3328        )
3329    }
3330
3331    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3332        self.width = size.map(|s| px(s));
3333        self.serialize(cx);
3334        cx.notify();
3335    }
3336
3337    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
3338        CollaborationPanelSettings::get_global(cx)
3339            .button
3340            .then(|| ui::Icon::Collab)
3341    }
3342
3343    fn toggle_action(&self) -> Box<dyn gpui::Action> {
3344        Box::new(ToggleFocus)
3345    }
3346
3347    fn has_focus(&self, cx: &gpui::WindowContext) -> bool {
3348        self.focus_handle.contains_focused(cx)
3349    }
3350
3351    fn persistent_name() -> &'static str {
3352        "CollabPanel"
3353    }
3354}
3355
3356impl FocusableView for CollabPanel {
3357    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
3358        self.filter_editor.focus_handle(cx).clone()
3359    }
3360}
3361
3362impl PartialEq for ListEntry {
3363    fn eq(&self, other: &Self) -> bool {
3364        match self {
3365            ListEntry::Header(section_1) => {
3366                if let ListEntry::Header(section_2) = other {
3367                    return section_1 == section_2;
3368                }
3369            }
3370            ListEntry::CallParticipant { user: user_1, .. } => {
3371                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3372                    return user_1.id == user_2.id;
3373                }
3374            }
3375            ListEntry::ParticipantProject {
3376                project_id: project_id_1,
3377                ..
3378            } => {
3379                if let ListEntry::ParticipantProject {
3380                    project_id: project_id_2,
3381                    ..
3382                } = other
3383                {
3384                    return project_id_1 == project_id_2;
3385                }
3386            }
3387            ListEntry::ParticipantScreen {
3388                peer_id: peer_id_1, ..
3389            } => {
3390                if let ListEntry::ParticipantScreen {
3391                    peer_id: peer_id_2, ..
3392                } = other
3393                {
3394                    return peer_id_1 == peer_id_2;
3395                }
3396            }
3397            ListEntry::Channel {
3398                channel: channel_1, ..
3399            } => {
3400                if let ListEntry::Channel {
3401                    channel: channel_2, ..
3402                } = other
3403                {
3404                    return channel_1.id == channel_2.id;
3405                }
3406            }
3407            ListEntry::ChannelNotes { channel_id } => {
3408                if let ListEntry::ChannelNotes {
3409                    channel_id: other_id,
3410                } = other
3411                {
3412                    return channel_id == other_id;
3413                }
3414            }
3415            ListEntry::ChannelChat { channel_id } => {
3416                if let ListEntry::ChannelChat {
3417                    channel_id: other_id,
3418                } = other
3419                {
3420                    return channel_id == other_id;
3421                }
3422            }
3423            // ListEntry::ChannelInvite(channel_1) => {
3424            //     if let ListEntry::ChannelInvite(channel_2) = other {
3425            //         return channel_1.id == channel_2.id;
3426            //     }
3427            // }
3428            ListEntry::IncomingRequest(user_1) => {
3429                if let ListEntry::IncomingRequest(user_2) = other {
3430                    return user_1.id == user_2.id;
3431                }
3432            }
3433            ListEntry::OutgoingRequest(user_1) => {
3434                if let ListEntry::OutgoingRequest(user_2) = other {
3435                    return user_1.id == user_2.id;
3436                }
3437            }
3438            ListEntry::Contact {
3439                contact: contact_1, ..
3440            } => {
3441                if let ListEntry::Contact {
3442                    contact: contact_2, ..
3443                } = other
3444                {
3445                    return contact_1.user.id == contact_2.user.id;
3446                }
3447            }
3448            ListEntry::ChannelEditor { depth } => {
3449                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3450                    return depth == other_depth;
3451                }
3452            }
3453            ListEntry::ContactPlaceholder => {
3454                if let ListEntry::ContactPlaceholder = other {
3455                    return true;
3456                }
3457            }
3458        }
3459        false
3460    }
3461}
3462
3463// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3464//     Svg::new(svg_path)
3465//         .with_color(style.color)
3466//         .constrained()
3467//         .with_width(style.icon_width)
3468//         .aligned()
3469//         .constrained()
3470//         .with_width(style.button_width)
3471//         .with_height(style.button_width)
3472//         .contained()
3473//         .with_style(style.container)
3474// }
3475
3476struct DraggedChannelView {
3477    channel: Channel,
3478    width: Pixels,
3479}
3480
3481impl Render for DraggedChannelView {
3482    type Element = Div;
3483
3484    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3485        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3486        h_stack()
3487            .font(ui_font)
3488            .bg(cx.theme().colors().background)
3489            .w(self.width)
3490            .p_1()
3491            .gap_1()
3492            .child(
3493                IconElement::new(
3494                    if self.channel.visibility == proto::ChannelVisibility::Public {
3495                        Icon::Public
3496                    } else {
3497                        Icon::Hash
3498                    },
3499                )
3500                .size(IconSize::Small)
3501                .color(Color::Muted),
3502            )
3503            .child(Label::new(self.channel.name.clone()))
3504    }
3505}