collab_panel.rs

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