collab_panel.rs

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