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