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(self.entries.iter().enumerate().map(|(ix, entry)| {
2163                        let is_selected = self.selection == Some(ix);
2164                        match entry {
2165                            ListEntry::Header(section) => {
2166                                let is_collapsed = self.collapsed_sections.contains(section);
2167                                self.render_header(*section, is_selected, is_collapsed, cx)
2168                                    .into_any_element()
2169                            }
2170                            ListEntry::Contact { contact, calling } => self
2171                                .render_contact(contact, *calling, is_selected, cx)
2172                                .into_any_element(),
2173                            ListEntry::ContactPlaceholder => self
2174                                .render_contact_placeholder(is_selected, cx)
2175                                .into_any_element(),
2176                            ListEntry::IncomingRequest(user) => self
2177                                .render_contact_request(user, true, is_selected, cx)
2178                                .into_any_element(),
2179                            ListEntry::OutgoingRequest(user) => self
2180                                .render_contact_request(user, false, is_selected, cx)
2181                                .into_any_element(),
2182                            ListEntry::Channel {
2183                                channel,
2184                                depth,
2185                                has_children,
2186                            } => self
2187                                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
2188                                .into_any_element(),
2189                            ListEntry::ChannelEditor { depth } => {
2190                                self.render_channel_editor(*depth, cx).into_any_element()
2191                            }
2192                            ListEntry::CallParticipant {
2193                                user,
2194                                peer_id,
2195                                is_pending,
2196                            } => self
2197                                .render_call_participant(user, *peer_id, *is_pending, cx)
2198                                .into_any_element(),
2199                            ListEntry::ParticipantProject {
2200                                project_id,
2201                                worktree_root_names,
2202                                host_user_id,
2203                                is_last,
2204                            } => self
2205                                .render_participant_project(
2206                                    *project_id,
2207                                    &worktree_root_names,
2208                                    *host_user_id,
2209                                    *is_last,
2210                                    cx,
2211                                )
2212                                .into_any_element(),
2213                            ListEntry::ParticipantScreen { peer_id, is_last } => self
2214                                .render_participant_screen(*peer_id, *is_last, cx)
2215                                .into_any_element(),
2216                            ListEntry::ChannelNotes { channel_id } => self
2217                                .render_channel_notes(*channel_id, cx)
2218                                .into_any_element(),
2219                            ListEntry::ChannelChat { channel_id } => {
2220                                self.render_channel_chat(*channel_id, cx).into_any_element()
2221                            }
2222                        }
2223                    })),
2224            )
2225    }
2226
2227    fn render_header(
2228        &self,
2229        section: Section,
2230        is_selected: bool,
2231        is_collapsed: bool,
2232        cx: &ViewContext<Self>,
2233    ) -> impl IntoElement {
2234        let mut channel_link = None;
2235        let mut channel_tooltip_text = None;
2236        let mut channel_icon = None;
2237        // let mut is_dragged_over = false;
2238
2239        let text = match section {
2240            Section::ActiveCall => {
2241                let channel_name = maybe!({
2242                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2243
2244                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2245
2246                    channel_link = Some(channel.link());
2247                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2248                        proto::ChannelVisibility::Public => {
2249                            (Some("icons/public.svg"), Some("Copy public channel link."))
2250                        }
2251                        proto::ChannelVisibility::Members => {
2252                            (Some("icons/hash.svg"), Some("Copy private channel link."))
2253                        }
2254                    };
2255
2256                    Some(channel.name.as_ref())
2257                });
2258
2259                if let Some(name) = channel_name {
2260                    SharedString::from(format!("{}", name))
2261                } else {
2262                    SharedString::from("Current Call")
2263                }
2264            }
2265            Section::ContactRequests => SharedString::from("Requests"),
2266            Section::Contacts => SharedString::from("Contacts"),
2267            Section::Channels => SharedString::from("Channels"),
2268            Section::ChannelInvites => SharedString::from("Invites"),
2269            Section::Online => SharedString::from("Online"),
2270            Section::Offline => SharedString::from("Offline"),
2271        };
2272
2273        let button = match section {
2274            Section::ActiveCall => channel_link.map(|channel_link| {
2275                let channel_link_copy = channel_link.clone();
2276                IconButton::new("channel-link", Icon::Copy)
2277                    .on_click(move |_, cx| {
2278                        let item = ClipboardItem::new(channel_link_copy.clone());
2279                        cx.write_to_clipboard(item)
2280                    })
2281                    .tooltip(|cx| Tooltip::text("Copy channel link", cx))
2282            }),
2283            Section::Contacts => Some(
2284                IconButton::new("add-contact", Icon::Plus)
2285                    .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2286                    .tooltip(|cx| Tooltip::text("Search for new contact", cx)),
2287            ),
2288            Section::Channels => Some(
2289                IconButton::new("add-channel", Icon::Plus)
2290                    .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
2291                    .tooltip(|cx| Tooltip::text("Create a channel", cx)),
2292            ),
2293            _ => None,
2294        };
2295
2296        let can_collapse = match section {
2297            Section::ActiveCall | Section::Channels | Section::Contacts => false,
2298            Section::ChannelInvites
2299            | Section::ContactRequests
2300            | Section::Online
2301            | Section::Offline => true,
2302        };
2303
2304        h_stack()
2305            .w_full()
2306            .map(|el| {
2307                if can_collapse {
2308                    el.child(
2309                        ListItem::new(text.clone())
2310                            .child(div().w_full().child(Label::new(text)))
2311                            .selected(is_selected)
2312                            .toggle(Some(!is_collapsed))
2313                            .on_click(cx.listener(move |this, _, cx| {
2314                                this.toggle_section_expanded(section, cx)
2315                            })),
2316                    )
2317                } else {
2318                    el.child(
2319                        ListHeader::new(text)
2320                            .when_some(button, |el, button| el.meta(button))
2321                            .selected(is_selected),
2322                    )
2323                }
2324            })
2325            .when(section == Section::Channels, |el| {
2326                el.drag_over::<DraggedChannelView>(|style| {
2327                    style.bg(cx.theme().colors().ghost_element_hover)
2328                })
2329                .on_drop(cx.listener(
2330                    move |this, view: &View<DraggedChannelView>, cx| {
2331                        this.channel_store
2332                            .update(cx, |channel_store, cx| {
2333                                channel_store.move_channel(view.read(cx).channel.id, None, cx)
2334                            })
2335                            .detach_and_log_err(cx)
2336                    },
2337                ))
2338            })
2339    }
2340
2341    fn render_contact(
2342        &self,
2343        contact: &Contact,
2344        calling: bool,
2345        is_selected: bool,
2346        cx: &mut ViewContext<Self>,
2347    ) -> impl IntoElement {
2348        let online = contact.online;
2349        let busy = contact.busy || calling;
2350        let user_id = contact.user.id;
2351        let github_login = SharedString::from(contact.user.github_login.clone());
2352        let mut item = ListItem::new(github_login.clone())
2353            .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2354            .child(
2355                h_stack()
2356                    .w_full()
2357                    .justify_between()
2358                    .child(Label::new(github_login.clone()))
2359                    .when(calling, |el| {
2360                        el.child(Label::new("Calling").color(Color::Muted))
2361                    })
2362                    .when(!calling, |el| {
2363                        el.child(
2364                            div()
2365                                .id("remove_contact")
2366                                .invisible()
2367                                .group_hover("", |style| style.visible())
2368                                .child(
2369                                    IconButton::new("remove_contact", Icon::Close)
2370                                        .icon_color(Color::Muted)
2371                                        .tooltip(|cx| Tooltip::text("Remove Contact", cx))
2372                                        .on_click(cx.listener({
2373                                            let github_login = github_login.clone();
2374                                            move |this, _, cx| {
2375                                                this.remove_contact(user_id, &github_login, cx);
2376                                            }
2377                                        })),
2378                                ),
2379                        )
2380                    }),
2381            )
2382            .left_child(
2383                // todo!() handle contacts with no avatar
2384                Avatar::data(contact.user.avatar.clone().unwrap())
2385                    .availability_indicator(if online { Some(!busy) } else { None }),
2386            )
2387            .when(online && !busy, |el| {
2388                el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2389            });
2390
2391        div()
2392            .id(github_login.clone())
2393            .group("")
2394            .child(item)
2395            .tooltip(move |cx| {
2396                let text = if !online {
2397                    format!(" {} is offline", &github_login)
2398                } else if busy {
2399                    format!(" {} is on a call", &github_login)
2400                } else {
2401                    let room = ActiveCall::global(cx).read(cx).room();
2402                    if room.is_some() {
2403                        format!("Invite {} to join call", &github_login)
2404                    } else {
2405                        format!("Call {}", &github_login)
2406                    }
2407                };
2408                Tooltip::text(text, cx)
2409            })
2410    }
2411
2412    fn render_contact_request(
2413        &self,
2414        user: &Arc<User>,
2415        is_incoming: bool,
2416        is_selected: bool,
2417        cx: &mut ViewContext<Self>,
2418    ) -> impl IntoElement {
2419        let github_login = SharedString::from(user.github_login.clone());
2420        let user_id = user.id;
2421        let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2422        let color = if is_contact_request_pending {
2423            Color::Muted
2424        } else {
2425            Color::Default
2426        };
2427
2428        let controls = if is_incoming {
2429            vec![
2430                IconButton::new("remove_contact", Icon::Close)
2431                    .on_click(cx.listener(move |this, _, cx| {
2432                        this.respond_to_contact_request(user_id, false, cx);
2433                    }))
2434                    .icon_color(color)
2435                    .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2436                IconButton::new("remove_contact", Icon::Check)
2437                    .on_click(cx.listener(move |this, _, cx| {
2438                        this.respond_to_contact_request(user_id, true, cx);
2439                    }))
2440                    .icon_color(color)
2441                    .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2442            ]
2443        } else {
2444            let github_login = github_login.clone();
2445            vec![IconButton::new("remove_contact", Icon::Close)
2446                .on_click(cx.listener(move |this, _, cx| {
2447                    this.remove_contact(user_id, &github_login, cx);
2448                }))
2449                .icon_color(color)
2450                .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2451        };
2452
2453        ListItem::new(github_login.clone())
2454            .child(
2455                h_stack()
2456                    .w_full()
2457                    .justify_between()
2458                    .child(Label::new(github_login.clone()))
2459                    .child(h_stack().children(controls)),
2460            )
2461            .when_some(user.avatar.clone(), |el, avatar| el.left_avatar(avatar))
2462    }
2463
2464    fn render_contact_placeholder(
2465        &self,
2466        is_selected: bool,
2467        cx: &mut ViewContext<Self>,
2468    ) -> impl IntoElement {
2469        ListItem::new("contact-placeholder")
2470            .child(IconElement::new(Icon::Plus))
2471            .child(Label::new("Add a Contact"))
2472            .selected(is_selected)
2473            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2474    }
2475
2476    fn render_channel(
2477        &self,
2478        channel: &Channel,
2479        depth: usize,
2480        has_children: bool,
2481        is_selected: bool,
2482        ix: usize,
2483        cx: &mut ViewContext<Self>,
2484    ) -> impl IntoElement {
2485        let channel_id = channel.id;
2486
2487        let is_active = maybe!({
2488            let call_channel = ActiveCall::global(cx)
2489                .read(cx)
2490                .room()?
2491                .read(cx)
2492                .channel_id()?;
2493            Some(call_channel == channel_id)
2494        })
2495        .unwrap_or(false);
2496        let is_public = self
2497            .channel_store
2498            .read(cx)
2499            .channel_for_id(channel_id)
2500            .map(|channel| channel.visibility)
2501            == Some(proto::ChannelVisibility::Public);
2502        let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2503        let disclosed =
2504            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2505
2506        let has_messages_notification = channel.unseen_message_id.is_some();
2507        let has_notes_notification = channel.unseen_note_version.is_some();
2508
2509        const FACEPILE_LIMIT: usize = 3;
2510        let participants = self.channel_store.read(cx).channel_participants(channel_id);
2511
2512        let face_pile = if !participants.is_empty() {
2513            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2514            let user = &participants[0];
2515
2516            let result = FacePile {
2517                faces: participants
2518                    .iter()
2519                    .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element()))
2520                    .take(FACEPILE_LIMIT)
2521                    .chain(if extra_count > 0 {
2522                        // todo!() @nate - this label looks wrong.
2523                        Some(Label::new(format!("+{}", extra_count)).into_any_element())
2524                    } else {
2525                        None
2526                    })
2527                    .collect::<Vec<_>>(),
2528            };
2529
2530            Some(result)
2531        } else {
2532            None
2533        };
2534
2535        let width = self.width.unwrap_or(px(240.));
2536
2537        div()
2538            .id(channel_id as usize)
2539            .group("")
2540            .on_drag({
2541                let channel = channel.clone();
2542                move |cx| {
2543                    let channel = channel.clone();
2544                    cx.build_view(|cx| DraggedChannelView { channel, width })
2545                }
2546            })
2547            .drag_over::<DraggedChannelView>(|style| {
2548                style.bg(cx.theme().colors().ghost_element_hover)
2549            })
2550            .on_drop(
2551                cx.listener(move |this, view: &View<DraggedChannelView>, cx| {
2552                    this.channel_store
2553                        .update(cx, |channel_store, cx| {
2554                            channel_store.move_channel(
2555                                view.read(cx).channel.id,
2556                                Some(channel_id),
2557                                cx,
2558                            )
2559                        })
2560                        .detach_and_log_err(cx)
2561                }),
2562            )
2563            .child(
2564                ListItem::new(channel_id as usize)
2565                    .indent_level(depth)
2566                    .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to  step over the disclosure toggle
2567                    .left_icon(if is_public { Icon::Public } else { Icon::Hash })
2568                    .selected(is_selected || is_active)
2569                    .child(
2570                        h_stack()
2571                            .w_full()
2572                            .justify_between()
2573                            .child(
2574                                h_stack()
2575                                    .id(channel_id as usize)
2576                                    .child(Label::new(channel.name.clone()))
2577                                    .children(face_pile.map(|face_pile| face_pile.render(cx))),
2578                            )
2579                            .child(
2580                                h_stack()
2581                                    .child(
2582                                        div()
2583                                            .id("channel_chat")
2584                                            .when(!has_messages_notification, |el| el.invisible())
2585                                            .group_hover("", |style| style.visible())
2586                                            .child(
2587                                                IconButton::new(
2588                                                    "channel_chat",
2589                                                    Icon::MessageBubbles,
2590                                                )
2591                                                .icon_color(if has_messages_notification {
2592                                                    Color::Default
2593                                                } else {
2594                                                    Color::Muted
2595                                                })
2596                                                .on_click(cx.listener(move |this, _, cx| {
2597                                                    this.join_channel_chat(channel_id, cx)
2598                                                }))
2599                                                .tooltip(|cx| {
2600                                                    Tooltip::text("Open channel chat", cx)
2601                                                }),
2602                                            ),
2603                                    )
2604                                    .child(
2605                                        div()
2606                                            .id("channel_notes")
2607                                            .when(!has_notes_notification, |el| el.invisible())
2608                                            .group_hover("", |style| style.visible())
2609                                            .child(
2610                                                IconButton::new("channel_notes", Icon::File)
2611                                                    .icon_color(if has_notes_notification {
2612                                                        Color::Default
2613                                                    } else {
2614                                                        Color::Muted
2615                                                    })
2616                                                    .on_click(cx.listener(move |this, _, cx| {
2617                                                        this.open_channel_notes(channel_id, cx)
2618                                                    }))
2619                                                    .tooltip(|cx| {
2620                                                        Tooltip::text("Open channel notes", cx)
2621                                                    }),
2622                                            ),
2623                                    ),
2624                            ),
2625                    )
2626                    .toggle(disclosed)
2627                    .on_toggle(
2628                        cx.listener(move |this, _, cx| {
2629                            this.toggle_channel_collapsed(channel_id, cx)
2630                        }),
2631                    )
2632                    .on_click(cx.listener(move |this, _, cx| {
2633                        if this.drag_target_channel == ChannelDragTarget::None {
2634                            if is_active {
2635                                this.open_channel_notes(channel_id, cx)
2636                            } else {
2637                                this.join_channel(channel_id, cx)
2638                            }
2639                        }
2640                    }))
2641                    .on_secondary_mouse_down(cx.listener(
2642                        move |this, event: &MouseDownEvent, cx| {
2643                            this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2644                        },
2645                    )),
2646            )
2647            .tooltip(|cx| Tooltip::text("Join channel", cx))
2648
2649        // let channel_id = channel.id;
2650        // let collab_theme = &theme.collab_panel;
2651        // let is_public = self
2652        //     .channel_store
2653        //     .read(cx)
2654        //     .channel_for_id(channel_id)
2655        //     .map(|channel| channel.visibility)
2656        //     == Some(proto::ChannelVisibility::Public);
2657        // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2658        // let disclosed =
2659        //     has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2660
2661        // enum ChannelCall {}
2662        // enum ChannelNote {}
2663        // enum NotesTooltip {}
2664        // enum ChatTooltip {}
2665        // enum ChannelTooltip {}
2666
2667        // let mut is_dragged_over = false;
2668        // if cx
2669        //     .global::<DragAndDrop<Workspace>>()
2670        //     .currently_dragged::<Channel>(cx.window())
2671        //     .is_some()
2672        //     && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
2673        // {
2674        //     is_dragged_over = true;
2675        // }
2676
2677        // let has_messages_notification = channel.unseen_message_id.is_some();
2678
2679        // MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
2680        //     let row_hovered = state.hovered();
2681
2682        //     let mut select_state = |interactive: &Interactive<ContainerStyle>| {
2683        //         if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
2684        //             interactive.clicked.as_ref().unwrap().clone()
2685        //         } else if state.hovered() || other_selected {
2686        //             interactive
2687        //                 .hovered
2688        //                 .as_ref()
2689        //                 .unwrap_or(&interactive.default)
2690        //                 .clone()
2691        //         } else {
2692        //             interactive.default.clone()
2693        //         }
2694        //     };
2695
2696        //     Flex::<Self>::row()
2697        //         .with_child(
2698        //             Svg::new(if is_public {
2699        //                 "icons/public.svg"
2700        //             } else {
2701        //                 "icons/hash.svg"
2702        //             })
2703        //             .with_color(collab_theme.channel_hash.color)
2704        //             .constrained()
2705        //             .with_width(collab_theme.channel_hash.width)
2706        //             .aligned()
2707        //             .left(),
2708        //         )
2709        //         .with_child({
2710        //             let style = collab_theme.channel_name.inactive_state();
2711        //             Flex::row()
2712        //                 .with_child(
2713        //                     Label::new(channel.name.clone(), style.text.clone())
2714        //                         .contained()
2715        //                         .with_style(style.container)
2716        //                         .aligned()
2717        //                         .left()
2718        //                         .with_tooltip::<ChannelTooltip>(
2719        //                             ix,
2720        //                             "Join channel",
2721        //                             None,
2722        //                             theme.tooltip.clone(),
2723        //                             cx,
2724        //                         ),
2725        //                 )
2726        //                 .with_children({
2727        //                     let participants =
2728        //                         self.channel_store.read(cx).channel_participants(channel_id);
2729
2730        //                     if !participants.is_empty() {
2731        //                         let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2732
2733        //                         let result = FacePile::new(collab_theme.face_overlap)
2734        //                             .with_children(
2735        //                                 participants
2736        //                                     .iter()
2737        //                                     .filter_map(|user| {
2738        //                                         Some(
2739        //                                             Image::from_data(user.avatar.clone()?)
2740        //                                                 .with_style(collab_theme.channel_avatar),
2741        //                                         )
2742        //                                     })
2743        //                                     .take(FACEPILE_LIMIT),
2744        //                             )
2745        //                             .with_children((extra_count > 0).then(|| {
2746        //                                 Label::new(
2747        //                                     format!("+{}", extra_count),
2748        //                                     collab_theme.extra_participant_label.text.clone(),
2749        //                                 )
2750        //                                 .contained()
2751        //                                 .with_style(collab_theme.extra_participant_label.container)
2752        //                             }));
2753
2754        //                         Some(result)
2755        //                     } else {
2756        //                         None
2757        //                     }
2758        //                 })
2759        //                 .with_spacing(8.)
2760        //                 .align_children_center()
2761        //                 .flex(1., true)
2762        //         })
2763        //         .with_child(
2764        //             MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
2765        //                 let container_style = collab_theme
2766        //                     .disclosure
2767        //                     .button
2768        //                     .style_for(mouse_state)
2769        //                     .container;
2770
2771        //                 if channel.unseen_message_id.is_some() {
2772        //                     Svg::new("icons/conversations.svg")
2773        //                         .with_color(collab_theme.channel_note_active_color)
2774        //                         .constrained()
2775        //                         .with_width(collab_theme.channel_hash.width)
2776        //                         .contained()
2777        //                         .with_style(container_style)
2778        //                         .with_uniform_padding(4.)
2779        //                         .into_any()
2780        //                 } else if row_hovered {
2781        //                     Svg::new("icons/conversations.svg")
2782        //                         .with_color(collab_theme.channel_hash.color)
2783        //                         .constrained()
2784        //                         .with_width(collab_theme.channel_hash.width)
2785        //                         .contained()
2786        //                         .with_style(container_style)
2787        //                         .with_uniform_padding(4.)
2788        //                         .into_any()
2789        //                 } else {
2790        //                     Empty::new().into_any()
2791        //                 }
2792        //             })
2793        //             .on_click(MouseButton::Left, move |_, this, cx| {
2794        //                 this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
2795        //             })
2796        //             .with_tooltip::<ChatTooltip>(
2797        //                 ix,
2798        //                 "Open channel chat",
2799        //                 None,
2800        //                 theme.tooltip.clone(),
2801        //                 cx,
2802        //             )
2803        //             .contained()
2804        //             .with_margin_right(4.),
2805        //         )
2806        //         .with_child(
2807        //             MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
2808        //                 let container_style = collab_theme
2809        //                     .disclosure
2810        //                     .button
2811        //                     .style_for(mouse_state)
2812        //                     .container;
2813        //                 if row_hovered || channel.unseen_note_version.is_some() {
2814        //                     Svg::new("icons/file.svg")
2815        //                         .with_color(if channel.unseen_note_version.is_some() {
2816        //                             collab_theme.channel_note_active_color
2817        //                         } else {
2818        //                             collab_theme.channel_hash.color
2819        //                         })
2820        //                         .constrained()
2821        //                         .with_width(collab_theme.channel_hash.width)
2822        //                         .contained()
2823        //                         .with_style(container_style)
2824        //                         .with_uniform_padding(4.)
2825        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2826        //                         .with_tooltip::<NotesTooltip>(
2827        //                             ix as usize,
2828        //                             "Open channel notes",
2829        //                             None,
2830        //                             theme.tooltip.clone(),
2831        //                             cx,
2832        //                         )
2833        //                         .into_any()
2834        //                 } else if has_messages_notification {
2835        //                     Empty::new()
2836        //                         .constrained()
2837        //                         .with_width(collab_theme.channel_hash.width)
2838        //                         .contained()
2839        //                         .with_uniform_padding(4.)
2840        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2841        //                         .into_any()
2842        //                 } else {
2843        //                     Empty::new().into_any()
2844        //                 }
2845        //             })
2846        //             .on_click(MouseButton::Left, move |_, this, cx| {
2847        //                 this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2848        //             }),
2849        //         )
2850        //         .align_children_center()
2851        //         .styleable_component()
2852        //         .disclosable(
2853        //             disclosed,
2854        //             Box::new(ToggleCollapse {
2855        //                 location: channel.id.clone(),
2856        //             }),
2857        //         )
2858        //         .with_id(ix)
2859        //         .with_style(collab_theme.disclosure.clone())
2860        //         .element()
2861        //         .constrained()
2862        //         .with_height(collab_theme.row_height)
2863        //         .contained()
2864        //         .with_style(select_state(
2865        //             collab_theme
2866        //                 .channel_row
2867        //                 .in_state(is_selected || is_active || is_dragged_over),
2868        //         ))
2869        //         .with_padding_left(
2870        //             collab_theme.channel_row.default_style().padding.left
2871        //                 + collab_theme.channel_indent * depth as f32,
2872        //         )
2873        // })
2874        // .on_click(MouseButton::Left, move |_, this, cx| {
2875        //     if this.
2876        // drag_target_channel == ChannelDragTarget::None {
2877        //         if is_active {
2878        //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
2879        //         } else {
2880        //             this.join_channel(channel_id, cx)
2881        //         }
2882        //     }
2883        // })
2884        // .on_click(MouseButton::Right, {
2885        //     let channel = channel.clone();
2886        //     move |e, this, cx| {
2887        //         this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
2888        //     }
2889        // })
2890        // .on_up(MouseButton::Left, move |_, this, cx| {
2891        //     if let Some((_, dragged_channel)) = cx
2892        //         .global::<DragAndDrop<Workspace>>()
2893        //         .currently_dragged::<Channel>(cx.window())
2894        //     {
2895        //         this.channel_store
2896        //             .update(cx, |channel_store, cx| {
2897        //                 channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2898        //             })
2899        //             .detach_and_log_err(cx)
2900        //     }
2901        // })
2902        // .on_move({
2903        //     let channel = channel.clone();
2904        //     move |_, this, cx| {
2905        //         if let Some((_, dragged_channel)) = cx
2906        //             .global::<DragAndDrop<Workspace>>()
2907        //             .currently_dragged::<Channel>(cx.window())
2908        //         {
2909        //             if channel.id != dragged_channel.id {
2910        //                 this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
2911        //             }
2912        //             cx.notify()
2913        //         }
2914        //     }
2915        // })
2916        // .as_draggable::<_, Channel>(
2917        //     channel.clone(),
2918        //     move |_, channel, cx: &mut ViewContext<Workspace>| {
2919        //         let theme = &theme::current(cx).collab_panel;
2920
2921        //         Flex::<Workspace>::row()
2922        //             .with_child(
2923        //                 Svg::new("icons/hash.svg")
2924        //                     .with_color(theme.channel_hash.color)
2925        //                     .constrained()
2926        //                     .with_width(theme.channel_hash.width)
2927        //                     .aligned()
2928        //                     .left(),
2929        //             )
2930        //             .with_child(
2931        //                 Label::new(channel.name.clone(), theme.channel_name.text.clone())
2932        //                     .contained()
2933        //                     .with_style(theme.channel_name.container)
2934        //                     .aligned()
2935        //                     .left(),
2936        //             )
2937        //             .align_children_center()
2938        //             .contained()
2939        //             .with_background_color(
2940        //                 theme
2941        //                     .container
2942        //                     .background_color
2943        //                     .unwrap_or(gpui::color::Color::transparent_black()),
2944        //             )
2945        //             .contained()
2946        //             .with_padding_left(
2947        //                 theme.channel_row.default_style().padding.left
2948        //                     + theme.channel_indent * depth as f32,
2949        //             )
2950        //             .into_any()
2951        //     },
2952        // )
2953        // .with_cursor_style(CursorStyle::PointingHand)
2954        // .into_any()
2955    }
2956
2957    fn render_channel_editor(&self, depth: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
2958        let item = ListItem::new("channel-editor")
2959            .inset(false)
2960            .indent_level(depth)
2961            .left_icon(Icon::Hash);
2962
2963        if let Some(pending_name) = self
2964            .channel_editing_state
2965            .as_ref()
2966            .and_then(|state| state.pending_name())
2967        {
2968            item.child(Label::new(pending_name))
2969        } else {
2970            item.child(
2971                div()
2972                    .w_full()
2973                    .py_1() // todo!() @nate this is a px off at the default font size.
2974                    .child(self.channel_name_editor.clone()),
2975            )
2976        }
2977    }
2978}
2979
2980fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
2981    let rem_size = cx.rem_size();
2982    let line_height = cx.text_style().line_height_in_pixels(rem_size);
2983    let width = rem_size * 1.5;
2984    let thickness = px(2.);
2985    let color = cx.theme().colors().text;
2986
2987    canvas(move |bounds, cx| {
2988        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2989        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2990        let right = bounds.right();
2991        let top = bounds.top();
2992
2993        cx.paint_quad(
2994            Bounds::from_corners(
2995                point(start_x, top),
2996                point(
2997                    start_x + thickness,
2998                    if is_last { start_y } else { bounds.bottom() },
2999                ),
3000            ),
3001            Default::default(),
3002            color,
3003            Default::default(),
3004            Hsla::transparent_black(),
3005        );
3006        cx.paint_quad(
3007            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
3008            Default::default(),
3009            color,
3010            Default::default(),
3011            Hsla::transparent_black(),
3012        );
3013    })
3014    .w(width)
3015    .h(line_height)
3016}
3017
3018impl Render for CollabPanel {
3019    type Element = Focusable<Div>;
3020
3021    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3022        v_stack()
3023            .key_context("CollabPanel")
3024            .on_action(cx.listener(CollabPanel::cancel))
3025            .on_action(cx.listener(CollabPanel::select_next))
3026            .on_action(cx.listener(CollabPanel::select_prev))
3027            .on_action(cx.listener(CollabPanel::confirm))
3028            .on_action(cx.listener(CollabPanel::insert_space))
3029            //     .on_action(cx.listener(CollabPanel::remove))
3030            .on_action(cx.listener(CollabPanel::remove_selected_channel))
3031            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
3032            //     .on_action(cx.listener(CollabPanel::new_subchannel))
3033            //     .on_action(cx.listener(CollabPanel::invite_members))
3034            //     .on_action(cx.listener(CollabPanel::manage_members))
3035            .on_action(cx.listener(CollabPanel::rename_selected_channel))
3036            //     .on_action(cx.listener(CollabPanel::rename_channel))
3037            //     .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action))
3038            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
3039            .on_action(cx.listener(CollabPanel::expand_selected_channel))
3040            //     .on_action(cx.listener(CollabPanel::open_channel_notes))
3041            //     .on_action(cx.listener(CollabPanel::join_channel_chat))
3042            //     .on_action(cx.listener(CollabPanel::copy_channel_link))
3043            .track_focus(&self.focus_handle)
3044            .size_full()
3045            .child(if self.user_store.read(cx).current_user().is_none() {
3046                self.render_signed_out(cx)
3047            } else {
3048                self.render_signed_in(cx)
3049            })
3050            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3051                overlay()
3052                    .position(*position)
3053                    .anchor(gpui::AnchorCorner::TopLeft)
3054                    .child(menu.clone())
3055            }))
3056    }
3057}
3058
3059// impl View for CollabPanel {
3060//     fn ui_name() -> &'static str {
3061//         "CollabPanel"
3062//     }
3063
3064//     fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
3065//         if !self.has_focus {
3066//             self.has_focus = true;
3067//             if !self.context_menu.is_focused(cx) {
3068//                 if let Some(editing_state) = &self.channel_editing_state {
3069//                     if editing_state.pending_name().is_none() {
3070//                         cx.focus(&self.channel_name_editor);
3071//                     } else {
3072//                         cx.focus(&self.filter_editor);
3073//                     }
3074//                 } else {
3075//                     cx.focus(&self.filter_editor);
3076//                 }
3077//             }
3078//             cx.emit(Event::Focus);
3079//         }
3080//     }
3081
3082//     fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
3083//         self.has_focus = false;
3084//     }
3085
3086//     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
3087//         let theme = &theme::current(cx).collab_panel;
3088
3089//         if self.user_store.read(cx).current_user().is_none() {
3090//             enum LogInButton {}
3091
3092//             return Flex::column()
3093//                 .with_child(
3094//                     MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
3095//                         let button = theme.log_in_button.style_for(state);
3096//                         Label::new("Sign in to collaborate", button.text.clone())
3097//                             .aligned()
3098//                             .left()
3099//                             .contained()
3100//                             .with_style(button.container)
3101//                     })
3102//                     .on_click(MouseButton::Left, |_, this, cx| {
3103//                         let client = this.client.clone();
3104//                         cx.spawn(|_, cx| async move {
3105//                             client.authenticate_and_connect(true, &cx).await.log_err();
3106//                         })
3107//                         .detach();
3108//                     })
3109//                     .with_cursor_style(CursorStyle::PointingHand),
3110//                 )
3111//                 .contained()
3112//                 .with_style(theme.container)
3113//                 .into_any();
3114//         }
3115
3116//         enum PanelFocus {}
3117//         MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
3118//             Stack::new()
3119//                 .with_child(
3120//                     Flex::column()
3121//                         .with_child(
3122//                             Flex::row().with_child(
3123//                                 ChildView::new(&self.filter_editor, cx)
3124//                                     .contained()
3125//                                     .with_style(theme.user_query_editor.container)
3126//                                     .flex(1.0, true),
3127//                             ),
3128//                         )
3129//                         .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3130//                         .contained()
3131//                         .with_style(theme.container)
3132//                         .into_any(),
3133//                 )
3134//                 .with_children(
3135//                     (!self.context_menu_on_selected)
3136//                         .then(|| ChildView::new(&self.context_menu, cx)),
3137//                 )
3138//                 .into_any()
3139//         })
3140//         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3141//         .into_any_named("collab panel")
3142//     }
3143
3144//     fn update_keymap_context(
3145//         &self,
3146//         keymap: &mut gpui::keymap_matcher::KeymapContext,
3147//         _: &AppContext,
3148//     ) {
3149//         Self::reset_to_default_keymap_context(keymap);
3150//         if self.channel_editing_state.is_some() {
3151//             keymap.add_identifier("editing");
3152//         } else {
3153//             keymap.add_identifier("not_editing");
3154//         }
3155//     }
3156// }
3157
3158impl EventEmitter<PanelEvent> for CollabPanel {}
3159
3160impl Panel for CollabPanel {
3161    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3162        CollaborationPanelSettings::get_global(cx).dock
3163    }
3164
3165    fn position_is_valid(&self, position: DockPosition) -> bool {
3166        matches!(position, DockPosition::Left | DockPosition::Right)
3167    }
3168
3169    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3170        settings::update_settings_file::<CollaborationPanelSettings>(
3171            self.fs.clone(),
3172            cx,
3173            move |settings| settings.dock = Some(position),
3174        );
3175    }
3176
3177    fn size(&self, cx: &gpui::WindowContext) -> f32 {
3178        self.width.map_or_else(
3179            || CollaborationPanelSettings::get_global(cx).default_width,
3180            |width| width.0,
3181        )
3182    }
3183
3184    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3185        self.width = size.map(|s| px(s));
3186        self.serialize(cx);
3187        cx.notify();
3188    }
3189
3190    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
3191        CollaborationPanelSettings::get_global(cx)
3192            .button
3193            .then(|| ui::Icon::Collab)
3194    }
3195
3196    fn toggle_action(&self) -> Box<dyn gpui::Action> {
3197        Box::new(ToggleFocus)
3198    }
3199
3200    fn persistent_name() -> &'static str {
3201        "CollabPanel"
3202    }
3203}
3204
3205impl FocusableView for CollabPanel {
3206    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
3207        self.filter_editor.focus_handle(cx).clone()
3208    }
3209}
3210
3211impl PartialEq for ListEntry {
3212    fn eq(&self, other: &Self) -> bool {
3213        match self {
3214            ListEntry::Header(section_1) => {
3215                if let ListEntry::Header(section_2) = other {
3216                    return section_1 == section_2;
3217                }
3218            }
3219            ListEntry::CallParticipant { user: user_1, .. } => {
3220                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3221                    return user_1.id == user_2.id;
3222                }
3223            }
3224            ListEntry::ParticipantProject {
3225                project_id: project_id_1,
3226                ..
3227            } => {
3228                if let ListEntry::ParticipantProject {
3229                    project_id: project_id_2,
3230                    ..
3231                } = other
3232                {
3233                    return project_id_1 == project_id_2;
3234                }
3235            }
3236            ListEntry::ParticipantScreen {
3237                peer_id: peer_id_1, ..
3238            } => {
3239                if let ListEntry::ParticipantScreen {
3240                    peer_id: peer_id_2, ..
3241                } = other
3242                {
3243                    return peer_id_1 == peer_id_2;
3244                }
3245            }
3246            ListEntry::Channel {
3247                channel: channel_1, ..
3248            } => {
3249                if let ListEntry::Channel {
3250                    channel: channel_2, ..
3251                } = other
3252                {
3253                    return channel_1.id == channel_2.id;
3254                }
3255            }
3256            ListEntry::ChannelNotes { channel_id } => {
3257                if let ListEntry::ChannelNotes {
3258                    channel_id: other_id,
3259                } = other
3260                {
3261                    return channel_id == other_id;
3262                }
3263            }
3264            ListEntry::ChannelChat { channel_id } => {
3265                if let ListEntry::ChannelChat {
3266                    channel_id: other_id,
3267                } = other
3268                {
3269                    return channel_id == other_id;
3270                }
3271            }
3272            // ListEntry::ChannelInvite(channel_1) => {
3273            //     if let ListEntry::ChannelInvite(channel_2) = other {
3274            //         return channel_1.id == channel_2.id;
3275            //     }
3276            // }
3277            ListEntry::IncomingRequest(user_1) => {
3278                if let ListEntry::IncomingRequest(user_2) = other {
3279                    return user_1.id == user_2.id;
3280                }
3281            }
3282            ListEntry::OutgoingRequest(user_1) => {
3283                if let ListEntry::OutgoingRequest(user_2) = other {
3284                    return user_1.id == user_2.id;
3285                }
3286            }
3287            ListEntry::Contact {
3288                contact: contact_1, ..
3289            } => {
3290                if let ListEntry::Contact {
3291                    contact: contact_2, ..
3292                } = other
3293                {
3294                    return contact_1.user.id == contact_2.user.id;
3295                }
3296            }
3297            ListEntry::ChannelEditor { depth } => {
3298                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3299                    return depth == other_depth;
3300                }
3301            }
3302            ListEntry::ContactPlaceholder => {
3303                if let ListEntry::ContactPlaceholder = other {
3304                    return true;
3305                }
3306            }
3307        }
3308        false
3309    }
3310}
3311
3312// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3313//     Svg::new(svg_path)
3314//         .with_color(style.color)
3315//         .constrained()
3316//         .with_width(style.icon_width)
3317//         .aligned()
3318//         .constrained()
3319//         .with_width(style.button_width)
3320//         .with_height(style.button_width)
3321//         .contained()
3322//         .with_style(style.container)
3323// }
3324
3325struct DraggedChannelView {
3326    channel: Channel,
3327    width: Pixels,
3328}
3329
3330impl Render for DraggedChannelView {
3331    type Element = Div;
3332
3333    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3334        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3335        h_stack()
3336            .font(ui_font)
3337            .bg(cx.theme().colors().background)
3338            .w(self.width)
3339            .p_1()
3340            .gap_1()
3341            .child(
3342                IconElement::new(
3343                    if self.channel.visibility == proto::ChannelVisibility::Public {
3344                        Icon::Public
3345                    } else {
3346                        Icon::Hash
3347                    },
3348                )
3349                .size(IconSize::Small)
3350                .color(Color::Muted),
3351            )
3352            .child(Label::new(self.channel.name.clone()))
3353    }
3354}