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::{face_pile::FacePile, CollaborationPanelSettings};
 195
 196use self::channel_modal::ChannelModal;
 197
 198pub fn init(cx: &mut AppContext) {
 199    cx.observe_new_views(|workspace: &mut Workspace, _| {
 200        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
 201            workspace.toggle_panel_focus::<CollabPanel>(cx);
 202        });
 203    })
 204    .detach();
 205    //     contact_finder::init(cx);
 206    //     channel_modal::init(cx);
 207    //     channel_view::init(cx);
 208
 209    //     cx.add_action(CollabPanel::cancel);
 210    //     cx.add_action(CollabPanel::select_next);
 211    //     cx.add_action(CollabPanel::select_prev);
 212    //     cx.add_action(CollabPanel::confirm);
 213    //     cx.add_action(CollabPanel::insert_space);
 214    //     cx.add_action(CollabPanel::remove);
 215    //     cx.add_action(CollabPanel::remove_selected_channel);
 216    //     cx.add_action(CollabPanel::show_inline_context_menu);
 217    //     cx.add_action(CollabPanel::new_subchannel);
 218    //     cx.add_action(CollabPanel::invite_members);
 219    //     cx.add_action(CollabPanel::manage_members);
 220    //     cx.add_action(CollabPanel::rename_selected_channel);
 221    //     cx.add_action(CollabPanel::rename_channel);
 222    //     cx.add_action(CollabPanel::toggle_channel_collapsed_action);
 223    //     cx.add_action(CollabPanel::collapse_selected_channel);
 224    //     cx.add_action(CollabPanel::expand_selected_channel);
 225    //     cx.add_action(CollabPanel::open_channel_notes);
 226    //     cx.add_action(CollabPanel::join_channel_chat);
 227    //     cx.add_action(CollabPanel::copy_channel_link);
 228
 229    //     cx.add_action(
 230    //         |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
 231    //             if panel.selection.take() != Some(action.ix) {
 232    //                 panel.selection = Some(action.ix)
 233    //             }
 234
 235    //             cx.notify();
 236    //         },
 237    //     );
 238
 239    //     cx.add_action(
 240    //         |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
 241    //             let Some(clipboard) = panel.channel_clipboard.take() else {
 242    //                 return;
 243    //             };
 244    //             let Some(selected_channel) = panel.selected_channel() else {
 245    //                 return;
 246    //             };
 247
 248    //             panel
 249    //                 .channel_store
 250    //                 .update(cx, |channel_store, cx| {
 251    //                     channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
 252    //                 })
 253    //                 .detach_and_log_err(cx)
 254    //         },
 255    //     );
 256
 257    //     cx.add_action(
 258    //         |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
 259    //             if let Some(clipboard) = panel.channel_clipboard.take() {
 260    //                 panel.channel_store.update(cx, |channel_store, cx| {
 261    //                     channel_store
 262    //                         .move_channel(clipboard.channel_id, Some(action.to), cx)
 263    //                         .detach_and_log_err(cx)
 264    //                 })
 265    //             }
 266    //         },
 267    //     );
 268}
 269
 270#[derive(Debug)]
 271pub enum ChannelEditingState {
 272    Create {
 273        location: Option<ChannelId>,
 274        pending_name: Option<String>,
 275    },
 276    Rename {
 277        location: ChannelId,
 278        pending_name: Option<String>,
 279    },
 280}
 281
 282impl ChannelEditingState {
 283    fn pending_name(&self) -> Option<String> {
 284        match self {
 285            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
 286            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
 287        }
 288    }
 289}
 290
 291pub struct CollabPanel {
 292    width: Option<Pixels>,
 293    fs: Arc<dyn Fs>,
 294    focus_handle: FocusHandle,
 295    channel_clipboard: Option<ChannelMoveClipboard>,
 296    pending_serialization: Task<Option<()>>,
 297    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
 298    filter_editor: View<Editor>,
 299    channel_name_editor: View<Editor>,
 300    channel_editing_state: Option<ChannelEditingState>,
 301    entries: Vec<ListEntry>,
 302    selection: Option<usize>,
 303    channel_store: Model<ChannelStore>,
 304    user_store: Model<UserStore>,
 305    client: Arc<Client>,
 306    project: Model<Project>,
 307    match_candidates: Vec<StringMatchCandidate>,
 308    scroll_handle: ScrollHandle,
 309    subscriptions: Vec<Subscription>,
 310    collapsed_sections: Vec<Section>,
 311    collapsed_channels: Vec<ChannelId>,
 312    drag_target_channel: ChannelDragTarget,
 313    workspace: WeakView<Workspace>,
 314    // context_menu_on_selected: bool,
 315}
 316
 317#[derive(PartialEq, Eq)]
 318enum ChannelDragTarget {
 319    None,
 320    Root,
 321    Channel(ChannelId),
 322}
 323
 324#[derive(Serialize, Deserialize)]
 325struct SerializedCollabPanel {
 326    width: Option<Pixels>,
 327    collapsed_channels: Option<Vec<u64>>,
 328}
 329
 330// #[derive(Debug)]
 331// pub enum Event {
 332//     DockPositionChanged,
 333//     Focus,
 334//     Dismissed,
 335// }
 336
 337#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 338enum Section {
 339    ActiveCall,
 340    Channels,
 341    ChannelInvites,
 342    ContactRequests,
 343    Contacts,
 344    Online,
 345    Offline,
 346}
 347
 348#[derive(Clone, Debug)]
 349enum ListEntry {
 350    Header(Section),
 351    CallParticipant {
 352        user: Arc<User>,
 353        peer_id: Option<PeerId>,
 354        is_pending: bool,
 355    },
 356    ParticipantProject {
 357        project_id: u64,
 358        worktree_root_names: Vec<String>,
 359        host_user_id: u64,
 360        is_last: bool,
 361    },
 362    ParticipantScreen {
 363        peer_id: Option<PeerId>,
 364        is_last: bool,
 365    },
 366    IncomingRequest(Arc<User>),
 367    OutgoingRequest(Arc<User>),
 368    //     ChannelInvite(Arc<Channel>),
 369    Channel {
 370        channel: Arc<Channel>,
 371        depth: usize,
 372        has_children: bool,
 373    },
 374    ChannelNotes {
 375        channel_id: ChannelId,
 376    },
 377    ChannelChat {
 378        channel_id: ChannelId,
 379    },
 380    ChannelEditor {
 381        depth: usize,
 382    },
 383    Contact {
 384        contact: Arc<Contact>,
 385        calling: bool,
 386    },
 387    ContactPlaceholder,
 388}
 389
 390impl CollabPanel {
 391    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 392        cx.build_view(|cx| {
 393            //             let view_id = cx.view_id();
 394
 395            let filter_editor = cx.build_view(|cx| {
 396                let mut editor = Editor::single_line(cx);
 397                editor.set_placeholder_text("Filter channels, contacts", cx);
 398                editor
 399            });
 400
 401            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
 402                if let editor::EditorEvent::BufferEdited = event {
 403                    let query = this.filter_editor.read(cx).text(cx);
 404                    if !query.is_empty() {
 405                        this.selection.take();
 406                    }
 407                    this.update_entries(true, cx);
 408                    if !query.is_empty() {
 409                        this.selection = this
 410                            .entries
 411                            .iter()
 412                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
 413                    }
 414                } else if let editor::EditorEvent::Blurred = event {
 415                    let query = this.filter_editor.read(cx).text(cx);
 416                    if query.is_empty() {
 417                        this.selection.take();
 418                        this.update_entries(true, cx);
 419                    }
 420                }
 421            })
 422            .detach();
 423
 424            let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx));
 425
 426            cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
 427                if let editor::EditorEvent::Blurred = event {
 428                    if let Some(state) = &this.channel_editing_state {
 429                        if state.pending_name().is_some() {
 430                            return;
 431                        }
 432                    }
 433                    this.take_editing_state(cx);
 434                    this.update_entries(false, cx);
 435                    cx.notify();
 436                }
 437            })
 438            .detach();
 439
 440            //             let list_state =
 441            //                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
 442            //                     let theme = theme::current(cx).clone();
 443            //                     let is_selected = this.selection == Some(ix);
 444            //                     let current_project_id = this.project.read(cx).remote_id();
 445
 446            //                     match &this.entries[ix] {
 447            //                         ListEntry::Header(section) => {
 448            //                             let is_collapsed = this.collapsed_sections.contains(section);
 449            //                             this.render_header(*section, &theme, is_selected, is_collapsed, cx)
 450            //                         }
 451            //                         ListEntry::CallParticipant {
 452            //                             user,
 453            //                             peer_id,
 454            //                             is_pending,
 455            //                         } => Self::render_call_participant(
 456            //                             user,
 457            //                             *peer_id,
 458            //                             this.user_store.clone(),
 459            //                             *is_pending,
 460            //                             is_selected,
 461            //                             &theme,
 462            //                             cx,
 463            //                         ),
 464            //                         ListEntry::ParticipantProject {
 465            //                             project_id,
 466            //                             worktree_root_names,
 467            //                             host_user_id,
 468            //                             is_last,
 469            //                         } => Self::render_participant_project(
 470            //                             *project_id,
 471            //                             worktree_root_names,
 472            //                             *host_user_id,
 473            //                             Some(*project_id) == current_project_id,
 474            //                             *is_last,
 475            //                             is_selected,
 476            //                             &theme,
 477            //                             cx,
 478            //                         ),
 479            //                         ListEntry::ParticipantScreen { peer_id, is_last } => {
 480            //                             Self::render_participant_screen(
 481            //                                 *peer_id,
 482            //                                 *is_last,
 483            //                                 is_selected,
 484            //                                 &theme.collab_panel,
 485            //                                 cx,
 486            //                             )
 487            //                         }
 488            //                         ListEntry::Channel {
 489            //                             channel,
 490            //                             depth,
 491            //                             has_children,
 492            //                         } => {
 493            //                             let channel_row = this.render_channel(
 494            //                                 &*channel,
 495            //                                 *depth,
 496            //                                 &theme,
 497            //                                 is_selected,
 498            //                                 *has_children,
 499            //                                 ix,
 500            //                                 cx,
 501            //                             );
 502
 503            //                             if is_selected && this.context_menu_on_selected {
 504            //                                 Stack::new()
 505            //                                     .with_child(channel_row)
 506            //                                     .with_child(
 507            //                                         ChildView::new(&this.context_menu, cx)
 508            //                                             .aligned()
 509            //                                             .bottom()
 510            //                                             .right(),
 511            //                                     )
 512            //                                     .into_any()
 513            //                             } else {
 514            //                                 return channel_row;
 515            //                             }
 516            //                         }
 517            //                         ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
 518            //                             *channel_id,
 519            //                             &theme.collab_panel,
 520            //                             is_selected,
 521            //                             ix,
 522            //                             cx,
 523            //                         ),
 524            //                         ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
 525            //                             *channel_id,
 526            //                             &theme.collab_panel,
 527            //                             is_selected,
 528            //                             ix,
 529            //                             cx,
 530            //                         ),
 531            //                         ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
 532            //                             channel.clone(),
 533            //                             this.channel_store.clone(),
 534            //                             &theme.collab_panel,
 535            //                             is_selected,
 536            //                             cx,
 537            //                         ),
 538            //                         ListEntry::IncomingRequest(user) => Self::render_contact_request(
 539            //                             user.clone(),
 540            //                             this.user_store.clone(),
 541            //                             &theme.collab_panel,
 542            //                             true,
 543            //                             is_selected,
 544            //                             cx,
 545            //                         ),
 546            //                         ListEntry::OutgoingRequest(user) => Self::render_contact_request(
 547            //                             user.clone(),
 548            //                             this.user_store.clone(),
 549            //                             &theme.collab_panel,
 550            //                             false,
 551            //                             is_selected,
 552            //                             cx,
 553            //                         ),
 554            //                         ListEntry::Contact { contact, calling } => Self::render_contact(
 555            //                             contact,
 556            //                             *calling,
 557            //                             &this.project,
 558            //                             &theme,
 559            //                             is_selected,
 560            //                             cx,
 561            //                         ),
 562            //                         ListEntry::ChannelEditor { depth } => {
 563            //                             this.render_channel_editor(&theme, *depth, cx)
 564            //                         }
 565            //                         ListEntry::ContactPlaceholder => {
 566            //                             this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
 567            //                         }
 568            //                     }
 569            //                 });
 570
 571            let mut this = Self {
 572                width: None,
 573                focus_handle: cx.focus_handle(),
 574                channel_clipboard: None,
 575                fs: workspace.app_state().fs.clone(),
 576                pending_serialization: Task::ready(None),
 577                context_menu: None,
 578                channel_name_editor,
 579                filter_editor,
 580                entries: Vec::default(),
 581                channel_editing_state: None,
 582                selection: None,
 583                channel_store: ChannelStore::global(cx),
 584                user_store: workspace.user_store().clone(),
 585                project: workspace.project().clone(),
 586                subscriptions: Vec::default(),
 587                match_candidates: Vec::default(),
 588                scroll_handle: ScrollHandle::new(),
 589                collapsed_sections: vec![Section::Offline],
 590                collapsed_channels: Vec::default(),
 591                workspace: workspace.weak_handle(),
 592                client: workspace.app_state().client.clone(),
 593                //                 context_menu_on_selected: true,
 594                drag_target_channel: ChannelDragTarget::None,
 595            };
 596
 597            this.update_entries(false, cx);
 598
 599            // Update the dock position when the setting changes.
 600            let mut old_dock_position = this.position(cx);
 601            this.subscriptions.push(cx.observe_global::<SettingsStore>(
 602                move |this: &mut Self, cx| {
 603                    let new_dock_position = this.position(cx);
 604                    if new_dock_position != old_dock_position {
 605                        old_dock_position = new_dock_position;
 606                        cx.emit(PanelEvent::ChangePosition);
 607                    }
 608                    cx.notify();
 609                },
 610            ));
 611
 612            let active_call = ActiveCall::global(cx);
 613            this.subscriptions
 614                .push(cx.observe(&this.user_store, |this, _, cx| {
 615                    this.update_entries(true, cx)
 616                }));
 617            this.subscriptions
 618                .push(cx.observe(&this.channel_store, |this, _, cx| {
 619                    this.update_entries(true, cx)
 620                }));
 621            this.subscriptions
 622                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
 623            this.subscriptions
 624                .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
 625                    this.update_entries(true, cx)
 626                }));
 627            this.subscriptions.push(cx.subscribe(
 628                &this.channel_store,
 629                |this, _channel_store, e, cx| match e {
 630                    ChannelEvent::ChannelCreated(channel_id)
 631                    | ChannelEvent::ChannelRenamed(channel_id) => {
 632                        if this.take_editing_state(cx) {
 633                            this.update_entries(false, cx);
 634                            this.selection = this.entries.iter().position(|entry| {
 635                                if let ListEntry::Channel { channel, .. } = entry {
 636                                    channel.id == *channel_id
 637                                } else {
 638                                    false
 639                                }
 640                            });
 641                        }
 642                    }
 643                },
 644            ));
 645
 646            this
 647        })
 648    }
 649
 650    fn contacts(&self, cx: &AppContext) -> Option<Vec<Arc<Contact>>> {
 651        Some(self.user_store.read(cx).contacts().to_owned())
 652    }
 653    pub async fn load(
 654        workspace: WeakView<Workspace>,
 655        mut cx: AsyncWindowContext,
 656    ) -> anyhow::Result<View<Self>> {
 657        let serialized_panel = cx
 658            .background_executor()
 659            .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
 660            .await
 661            .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
 662            .log_err()
 663            .flatten()
 664            .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
 665            .transpose()
 666            .log_err()
 667            .flatten();
 668
 669        workspace.update(&mut cx, |workspace, cx| {
 670            let panel = CollabPanel::new(workspace, cx);
 671            if let Some(serialized_panel) = serialized_panel {
 672                panel.update(cx, |panel, cx| {
 673                    panel.width = serialized_panel.width;
 674                    panel.collapsed_channels = serialized_panel
 675                        .collapsed_channels
 676                        .unwrap_or_else(|| Vec::new());
 677                    cx.notify();
 678                });
 679            }
 680            panel
 681        })
 682    }
 683
 684    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 685        let width = self.width;
 686        let collapsed_channels = self.collapsed_channels.clone();
 687        self.pending_serialization = cx.background_executor().spawn(
 688            async move {
 689                KEY_VALUE_STORE
 690                    .write_kvp(
 691                        COLLABORATION_PANEL_KEY.into(),
 692                        serde_json::to_string(&SerializedCollabPanel {
 693                            width,
 694                            collapsed_channels: Some(collapsed_channels),
 695                        })?,
 696                    )
 697                    .await?;
 698                anyhow::Ok(())
 699            }
 700            .log_err(),
 701        );
 702    }
 703
 704    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
 705        let channel_store = self.channel_store.read(cx);
 706        let user_store = self.user_store.read(cx);
 707        let query = self.filter_editor.read(cx).text(cx);
 708        let executor = cx.background_executor().clone();
 709
 710        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 711        let old_entries = mem::take(&mut self.entries);
 712        let mut scroll_to_top = false;
 713
 714        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 715            self.entries.push(ListEntry::Header(Section::ActiveCall));
 716            if !old_entries
 717                .iter()
 718                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
 719            {
 720                scroll_to_top = true;
 721            }
 722
 723            if !self.collapsed_sections.contains(&Section::ActiveCall) {
 724                let room = room.read(cx);
 725
 726                if let Some(channel_id) = room.channel_id() {
 727                    self.entries.push(ListEntry::ChannelNotes { channel_id });
 728                    self.entries.push(ListEntry::ChannelChat { channel_id })
 729                }
 730
 731                // Populate the active user.
 732                if let Some(user) = user_store.current_user() {
 733                    self.match_candidates.clear();
 734                    self.match_candidates.push(StringMatchCandidate {
 735                        id: 0,
 736                        string: user.github_login.clone(),
 737                        char_bag: user.github_login.chars().collect(),
 738                    });
 739                    let matches = executor.block(match_strings(
 740                        &self.match_candidates,
 741                        &query,
 742                        true,
 743                        usize::MAX,
 744                        &Default::default(),
 745                        executor.clone(),
 746                    ));
 747                    if !matches.is_empty() {
 748                        let user_id = user.id;
 749                        self.entries.push(ListEntry::CallParticipant {
 750                            user,
 751                            peer_id: None,
 752                            is_pending: false,
 753                        });
 754                        let mut projects = room.local_participant().projects.iter().peekable();
 755                        while let Some(project) = projects.next() {
 756                            self.entries.push(ListEntry::ParticipantProject {
 757                                project_id: project.id,
 758                                worktree_root_names: project.worktree_root_names.clone(),
 759                                host_user_id: user_id,
 760                                is_last: projects.peek().is_none() && !room.is_screen_sharing(),
 761                            });
 762                        }
 763                        if room.is_screen_sharing() {
 764                            self.entries.push(ListEntry::ParticipantScreen {
 765                                peer_id: None,
 766                                is_last: true,
 767                            });
 768                        }
 769                    }
 770                }
 771
 772                // Populate remote participants.
 773                self.match_candidates.clear();
 774                self.match_candidates
 775                    .extend(room.remote_participants().iter().map(|(_, participant)| {
 776                        StringMatchCandidate {
 777                            id: participant.user.id as usize,
 778                            string: participant.user.github_login.clone(),
 779                            char_bag: participant.user.github_login.chars().collect(),
 780                        }
 781                    }));
 782                let matches = executor.block(match_strings(
 783                    &self.match_candidates,
 784                    &query,
 785                    true,
 786                    usize::MAX,
 787                    &Default::default(),
 788                    executor.clone(),
 789                ));
 790                for mat in matches {
 791                    let user_id = mat.candidate_id as u64;
 792                    let participant = &room.remote_participants()[&user_id];
 793                    self.entries.push(ListEntry::CallParticipant {
 794                        user: participant.user.clone(),
 795                        peer_id: Some(participant.peer_id),
 796                        is_pending: false,
 797                    });
 798                    let mut projects = participant.projects.iter().peekable();
 799                    while let Some(project) = projects.next() {
 800                        self.entries.push(ListEntry::ParticipantProject {
 801                            project_id: project.id,
 802                            worktree_root_names: project.worktree_root_names.clone(),
 803                            host_user_id: participant.user.id,
 804                            is_last: projects.peek().is_none()
 805                                && participant.video_tracks.is_empty(),
 806                        });
 807                    }
 808                    if !participant.video_tracks.is_empty() {
 809                        self.entries.push(ListEntry::ParticipantScreen {
 810                            peer_id: Some(participant.peer_id),
 811                            is_last: true,
 812                        });
 813                    }
 814                }
 815
 816                // Populate pending participants.
 817                self.match_candidates.clear();
 818                self.match_candidates
 819                    .extend(room.pending_participants().iter().enumerate().map(
 820                        |(id, participant)| StringMatchCandidate {
 821                            id,
 822                            string: participant.github_login.clone(),
 823                            char_bag: participant.github_login.chars().collect(),
 824                        },
 825                    ));
 826                let matches = executor.block(match_strings(
 827                    &self.match_candidates,
 828                    &query,
 829                    true,
 830                    usize::MAX,
 831                    &Default::default(),
 832                    executor.clone(),
 833                ));
 834                self.entries
 835                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
 836                        user: room.pending_participants()[mat.candidate_id].clone(),
 837                        peer_id: None,
 838                        is_pending: true,
 839                    }));
 840            }
 841        }
 842
 843        let mut request_entries = Vec::new();
 844
 845        if cx.has_flag::<ChannelsAlpha>() {
 846            self.entries.push(ListEntry::Header(Section::Channels));
 847
 848            if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
 849                self.match_candidates.clear();
 850                self.match_candidates
 851                    .extend(channel_store.ordered_channels().enumerate().map(
 852                        |(ix, (_, channel))| StringMatchCandidate {
 853                            id: ix,
 854                            string: channel.name.clone(),
 855                            char_bag: channel.name.chars().collect(),
 856                        },
 857                    ));
 858                let matches = executor.block(match_strings(
 859                    &self.match_candidates,
 860                    &query,
 861                    true,
 862                    usize::MAX,
 863                    &Default::default(),
 864                    executor.clone(),
 865                ));
 866                if let Some(state) = &self.channel_editing_state {
 867                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
 868                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
 869                    }
 870                }
 871                let mut collapse_depth = None;
 872                for mat in matches {
 873                    let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
 874                    let depth = channel.parent_path.len();
 875
 876                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
 877                        collapse_depth = Some(depth);
 878                    } else if let Some(collapsed_depth) = collapse_depth {
 879                        if depth > collapsed_depth {
 880                            continue;
 881                        }
 882                        if self.is_channel_collapsed(channel.id) {
 883                            collapse_depth = Some(depth);
 884                        } else {
 885                            collapse_depth = None;
 886                        }
 887                    }
 888
 889                    let has_children = channel_store
 890                        .channel_at_index(mat.candidate_id + 1)
 891                        .map_or(false, |next_channel| {
 892                            next_channel.parent_path.ends_with(&[channel.id])
 893                        });
 894
 895                    match &self.channel_editing_state {
 896                        Some(ChannelEditingState::Create {
 897                            location: parent_id,
 898                            ..
 899                        }) if *parent_id == Some(channel.id) => {
 900                            self.entries.push(ListEntry::Channel {
 901                                channel: channel.clone(),
 902                                depth,
 903                                has_children: false,
 904                            });
 905                            self.entries
 906                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
 907                        }
 908                        Some(ChannelEditingState::Rename {
 909                            location: parent_id,
 910                            ..
 911                        }) if parent_id == &channel.id => {
 912                            self.entries.push(ListEntry::ChannelEditor { depth });
 913                        }
 914                        _ => {
 915                            self.entries.push(ListEntry::Channel {
 916                                channel: channel.clone(),
 917                                depth,
 918                                has_children,
 919                            });
 920                        }
 921                    }
 922                }
 923            }
 924
 925            //             let channel_invites = channel_store.channel_invitations();
 926            //             if !channel_invites.is_empty() {
 927            //                 self.match_candidates.clear();
 928            //                 self.match_candidates
 929            //                     .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
 930            //                         StringMatchCandidate {
 931            //                             id: ix,
 932            //                             string: channel.name.clone(),
 933            //                             char_bag: channel.name.chars().collect(),
 934            //                         }
 935            //                     }));
 936            //                 let matches = executor.block(match_strings(
 937            //                     &self.match_candidates,
 938            //                     &query,
 939            //                     true,
 940            //                     usize::MAX,
 941            //                     &Default::default(),
 942            //                     executor.clone(),
 943            //                 ));
 944            //                 request_entries.extend(matches.iter().map(|mat| {
 945            //                     ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
 946            //                 }));
 947
 948            //                 if !request_entries.is_empty() {
 949            //                     self.entries
 950            //                         .push(ListEntry::Header(Section::ChannelInvites));
 951            //                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
 952            //                         self.entries.append(&mut request_entries);
 953            //                     }
 954            //                 }
 955            //             }
 956        }
 957
 958        self.entries.push(ListEntry::Header(Section::Contacts));
 959
 960        request_entries.clear();
 961        let incoming = user_store.incoming_contact_requests();
 962        if !incoming.is_empty() {
 963            self.match_candidates.clear();
 964            self.match_candidates
 965                .extend(
 966                    incoming
 967                        .iter()
 968                        .enumerate()
 969                        .map(|(ix, user)| StringMatchCandidate {
 970                            id: ix,
 971                            string: user.github_login.clone(),
 972                            char_bag: user.github_login.chars().collect(),
 973                        }),
 974                );
 975            let matches = executor.block(match_strings(
 976                &self.match_candidates,
 977                &query,
 978                true,
 979                usize::MAX,
 980                &Default::default(),
 981                executor.clone(),
 982            ));
 983            request_entries.extend(
 984                matches
 985                    .iter()
 986                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
 987            );
 988        }
 989
 990        let outgoing = user_store.outgoing_contact_requests();
 991        if !outgoing.is_empty() {
 992            self.match_candidates.clear();
 993            self.match_candidates
 994                .extend(
 995                    outgoing
 996                        .iter()
 997                        .enumerate()
 998                        .map(|(ix, user)| StringMatchCandidate {
 999                            id: ix,
1000                            string: user.github_login.clone(),
1001                            char_bag: user.github_login.chars().collect(),
1002                        }),
1003                );
1004            let matches = executor.block(match_strings(
1005                &self.match_candidates,
1006                &query,
1007                true,
1008                usize::MAX,
1009                &Default::default(),
1010                executor.clone(),
1011            ));
1012            request_entries.extend(
1013                matches
1014                    .iter()
1015                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
1016            );
1017        }
1018
1019        if !request_entries.is_empty() {
1020            self.entries
1021                .push(ListEntry::Header(Section::ContactRequests));
1022            if !self.collapsed_sections.contains(&Section::ContactRequests) {
1023                self.entries.append(&mut request_entries);
1024            }
1025        }
1026
1027        let contacts = user_store.contacts();
1028        if !contacts.is_empty() {
1029            self.match_candidates.clear();
1030            self.match_candidates
1031                .extend(
1032                    contacts
1033                        .iter()
1034                        .enumerate()
1035                        .map(|(ix, contact)| StringMatchCandidate {
1036                            id: ix,
1037                            string: contact.user.github_login.clone(),
1038                            char_bag: contact.user.github_login.chars().collect(),
1039                        }),
1040                );
1041
1042            let matches = executor.block(match_strings(
1043                &self.match_candidates,
1044                &query,
1045                true,
1046                usize::MAX,
1047                &Default::default(),
1048                executor.clone(),
1049            ));
1050
1051            let (online_contacts, offline_contacts) = matches
1052                .iter()
1053                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
1054
1055            for (matches, section) in [
1056                (online_contacts, Section::Online),
1057                (offline_contacts, Section::Offline),
1058            ] {
1059                if !matches.is_empty() {
1060                    self.entries.push(ListEntry::Header(section));
1061                    if !self.collapsed_sections.contains(&section) {
1062                        let active_call = &ActiveCall::global(cx).read(cx);
1063                        for mat in matches {
1064                            let contact = &contacts[mat.candidate_id];
1065                            self.entries.push(ListEntry::Contact {
1066                                contact: contact.clone(),
1067                                calling: active_call.pending_invites().contains(&contact.user.id),
1068                            });
1069                        }
1070                    }
1071                }
1072            }
1073        }
1074
1075        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
1076            self.entries.push(ListEntry::ContactPlaceholder);
1077        }
1078
1079        if select_same_item {
1080            if let Some(prev_selected_entry) = prev_selected_entry {
1081                self.selection.take();
1082                for (ix, entry) in self.entries.iter().enumerate() {
1083                    if *entry == prev_selected_entry {
1084                        self.selection = Some(ix);
1085                        self.scroll_handle.scroll_to_item(ix);
1086                        break;
1087                    }
1088                }
1089            }
1090        } else {
1091            self.selection = self.selection.and_then(|prev_selection| {
1092                if self.entries.is_empty() {
1093                    None
1094                } else {
1095                    let ix = prev_selection.min(self.entries.len() - 1);
1096                    self.scroll_handle.scroll_to_item(ix);
1097                    Some(ix)
1098                }
1099            });
1100        }
1101
1102        if scroll_to_top {
1103            self.scroll_handle.scroll_to_item(0)
1104        } else {
1105            let (old_index, old_offset) = self.scroll_handle.logical_scroll_top();
1106            // Attempt to maintain the same scroll position.
1107            if let Some(old_top_entry) = old_entries.get(old_index) {
1108                let (new_index, new_offset) = self
1109                    .entries
1110                    .iter()
1111                    .position(|entry| entry == old_top_entry)
1112                    .map(|item_ix| (item_ix, old_offset))
1113                    .or_else(|| {
1114                        let entry_after_old_top = old_entries.get(old_index + 1)?;
1115                        let item_ix = self
1116                            .entries
1117                            .iter()
1118                            .position(|entry| entry == entry_after_old_top)?;
1119                        Some((item_ix, px(0.)))
1120                    })
1121                    .or_else(|| {
1122                        let entry_before_old_top = old_entries.get(old_index.saturating_sub(1))?;
1123                        let item_ix = self
1124                            .entries
1125                            .iter()
1126                            .position(|entry| entry == entry_before_old_top)?;
1127                        Some((item_ix, px(0.)))
1128                    })
1129                    .unwrap_or_else(|| (old_index, old_offset));
1130
1131                self.scroll_handle
1132                    .set_logical_scroll_top(new_index, new_offset);
1133            }
1134        }
1135
1136        cx.notify();
1137    }
1138
1139    fn render_call_participant(
1140        &self,
1141        user: Arc<User>,
1142        peer_id: Option<PeerId>,
1143        is_pending: bool,
1144        cx: &mut ViewContext<Self>,
1145    ) -> impl IntoElement {
1146        let is_current_user =
1147            self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
1148        let tooltip = format!("Follow {}", user.github_login);
1149
1150        ListItem::new(SharedString::from(user.github_login.clone()))
1151            .left_child(Avatar::data(user.avatar.clone().unwrap()))
1152            .child(
1153                h_stack()
1154                    .w_full()
1155                    .justify_between()
1156                    .child(Label::new(user.github_login.clone()))
1157                    .child(if is_pending {
1158                        Label::new("Calling").color(Color::Muted).into_any_element()
1159                    } else if is_current_user {
1160                        IconButton::new("leave-call", Icon::ArrowRight)
1161                            .on_click(cx.listener(move |this, _, cx| {
1162                                Self::leave_call(cx);
1163                            }))
1164                            .tooltip(|cx| Tooltip::text("Leave Call", cx))
1165                            .into_any_element()
1166                    } else {
1167                        div().into_any_element()
1168                    }),
1169            )
1170            .when_some(peer_id, |this, peer_id| {
1171                this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
1172                    .on_click(cx.listener(move |this, _, cx| {
1173                        this.workspace
1174                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx));
1175                    }))
1176            })
1177    }
1178
1179    fn render_participant_project(
1180        &self,
1181        project_id: u64,
1182        worktree_root_names: &[String],
1183        host_user_id: u64,
1184        //         is_current: bool,
1185        is_last: bool,
1186        //         is_selected: bool,
1187        //         theme: &theme::Theme,
1188        cx: &mut ViewContext<Self>,
1189    ) -> impl IntoElement {
1190        let project_name: SharedString = if worktree_root_names.is_empty() {
1191            "untitled".to_string()
1192        } else {
1193            worktree_root_names.join(", ")
1194        }
1195        .into();
1196
1197        let theme = cx.theme();
1198
1199        ListItem::new(project_id as usize)
1200            .on_click(cx.listener(move |this, _, cx| {
1201                this.workspace.update(cx, |workspace, cx| {
1202                    let app_state = workspace.app_state().clone();
1203                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1204                        .detach_and_log_err(cx);
1205                });
1206            }))
1207            .left_child(render_tree_branch(is_last, cx))
1208            .child(IconButton::new(0, Icon::Folder))
1209            .child(Label::new(project_name.clone()))
1210            .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
1211
1212        //         enum JoinProject {}
1213        //         enum JoinProjectTooltip {}
1214
1215        //         let collab_theme = &theme.collab_panel;
1216        //         let host_avatar_width = collab_theme
1217        //             .contact_avatar
1218        //             .width
1219        //             .or(collab_theme.contact_avatar.height)
1220        //             .unwrap_or(0.);
1221        //         let tree_branch = collab_theme.tree_branch;
1222
1223        //         let content =
1224        //             MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
1225        //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1226        //                 let row = if is_current {
1227        //                     collab_theme
1228        //                         .project_row
1229        //                         .in_state(true)
1230        //                         .style_for(&mut Default::default())
1231        //                 } else {
1232        //                     collab_theme
1233        //                         .project_row
1234        //                         .in_state(is_selected)
1235        //                         .style_for(mouse_state)
1236        //                 };
1237
1238        //                 Flex::row()
1239        //                     .with_child(render_tree_branch(
1240        //                         tree_branch,
1241        //                         &row.name.text,
1242        //                         is_last,
1243        //                         vec2f(host_avatar_width, collab_theme.row_height),
1244        //                         cx.font_cache(),
1245        //                     ))
1246        //                     .with_child(
1247        //                         Svg::new("icons/file_icons/folder.svg")
1248        //                             .with_color(collab_theme.channel_hash.color)
1249        //                             .constrained()
1250        //                             .with_width(collab_theme.channel_hash.width)
1251        //                             .aligned()
1252        //                             .left(),
1253        //                     )
1254        //                     .with_child(
1255        //                         Label::new(project_name.clone(), row.name.text.clone())
1256        //                             .aligned()
1257        //                             .left()
1258        //                             .contained()
1259        //                             .with_style(row.name.container)
1260        //                             .flex(1., false),
1261        //                     )
1262        //                     .constrained()
1263        //                     .with_height(collab_theme.row_height)
1264        //                     .contained()
1265        //                     .with_style(row.container)
1266        //             });
1267
1268        //         if is_current {
1269        //             return content.into_any();
1270        //         }
1271
1272        //         content
1273        //             .with_cursor_style(CursorStyle::PointingHand)
1274        //             .on_click(MouseButton::Left, move |_, this, cx| {
1275        //                 if let Some(workspace) = this.workspace.upgrade(cx) {
1276        //                     let app_state = workspace.read(cx).app_state().clone();
1277        //                     workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1278        //                         .detach_and_log_err(cx);
1279        //                 }
1280        //             })
1281        //             .with_tooltip::<JoinProjectTooltip>(
1282        //                 project_id as usize,
1283        //                 format!("Open {}", project_name),
1284        //                 None,
1285        //                 theme.tooltip.clone(),
1286        //                 cx,
1287        //             )
1288        //             .into_any()
1289    }
1290
1291    fn render_participant_screen(
1292        &self,
1293        peer_id: Option<PeerId>,
1294        is_last: bool,
1295        cx: &mut ViewContext<Self>,
1296    ) -> impl IntoElement {
1297        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
1298
1299        ListItem::new(("screen", id))
1300            .left_child(render_tree_branch(is_last, cx))
1301            .child(IconButton::new(0, Icon::Screen))
1302            .child(Label::new("Screen"))
1303            .when_some(peer_id, |this, _| {
1304                this.on_click(cx.listener(move |this, _, cx| {
1305                    this.workspace.update(cx, |workspace, cx| {
1306                        workspace.open_shared_screen(peer_id.unwrap(), cx)
1307                    });
1308                }))
1309                .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
1310            })
1311    }
1312
1313    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1314        if let Some(_) = self.channel_editing_state.take() {
1315            self.channel_name_editor.update(cx, |editor, cx| {
1316                editor.set_text("", cx);
1317            });
1318            true
1319        } else {
1320            false
1321        }
1322    }
1323
1324    //     fn render_contact_placeholder(
1325    //         &self,
1326    //         theme: &theme::CollabPanel,
1327    //         is_selected: bool,
1328    //         cx: &mut ViewContext<Self>,
1329    //     ) -> AnyElement<Self> {
1330    //         enum AddContacts {}
1331    //         MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1332    //             let style = theme.list_empty_state.style_for(is_selected, state);
1333    //             Flex::row()
1334    //                 .with_child(
1335    //                     Svg::new("icons/plus.svg")
1336    //                         .with_color(theme.list_empty_icon.color)
1337    //                         .constrained()
1338    //                         .with_width(theme.list_empty_icon.width)
1339    //                         .aligned()
1340    //                         .left(),
1341    //                 )
1342    //                 .with_child(
1343    //                     Label::new("Add a contact", style.text.clone())
1344    //                         .contained()
1345    //                         .with_style(theme.list_empty_label_container),
1346    //                 )
1347    //                 .align_children_center()
1348    //                 .contained()
1349    //                 .with_style(style.container)
1350    //                 .into_any()
1351    //         })
1352    //         .on_click(MouseButton::Left, |_, this, cx| {
1353    //             this.toggle_contact_finder(cx);
1354    //         })
1355    //         .into_any()
1356    //     }
1357
1358    fn render_channel_notes(
1359        &self,
1360        channel_id: ChannelId,
1361        cx: &mut ViewContext<Self>,
1362    ) -> impl IntoElement {
1363        ListItem::new("channel-notes")
1364            .on_click(cx.listener(move |this, _, cx| {
1365                this.open_channel_notes(channel_id, cx);
1366            }))
1367            .left_child(render_tree_branch(false, cx))
1368            .child(IconButton::new(0, Icon::File))
1369            .child(Label::new("notes"))
1370            .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
1371    }
1372
1373    fn render_channel_chat(
1374        &self,
1375        channel_id: ChannelId,
1376        cx: &mut ViewContext<Self>,
1377    ) -> impl IntoElement {
1378        ListItem::new("channel-chat")
1379            .on_click(cx.listener(move |this, _, cx| {
1380                this.join_channel_chat(channel_id, cx);
1381            }))
1382            .left_child(render_tree_branch(true, cx))
1383            .child(IconButton::new(0, Icon::MessageBubbles))
1384            .child(Label::new("chat"))
1385            .tooltip(move |cx| Tooltip::text("Open Chat", cx))
1386    }
1387
1388    //     fn render_channel_invite(
1389    //         channel: Arc<Channel>,
1390    //         channel_store: ModelHandle<ChannelStore>,
1391    //         theme: &theme::CollabPanel,
1392    //         is_selected: bool,
1393    //         cx: &mut ViewContext<Self>,
1394    //     ) -> AnyElement<Self> {
1395    //         enum Decline {}
1396    //         enum Accept {}
1397
1398    //         let channel_id = channel.id;
1399    //         let is_invite_pending = channel_store
1400    //             .read(cx)
1401    //             .has_pending_channel_invite_response(&channel);
1402    //         let button_spacing = theme.contact_button_spacing;
1403
1404    //         Flex::row()
1405    //             .with_child(
1406    //                 Svg::new("icons/hash.svg")
1407    //                     .with_color(theme.channel_hash.color)
1408    //                     .constrained()
1409    //                     .with_width(theme.channel_hash.width)
1410    //                     .aligned()
1411    //                     .left(),
1412    //             )
1413    //             .with_child(
1414    //                 Label::new(channel.name.clone(), theme.contact_username.text.clone())
1415    //                     .contained()
1416    //                     .with_style(theme.contact_username.container)
1417    //                     .aligned()
1418    //                     .left()
1419    //                     .flex(1., true),
1420    //             )
1421    //             .with_child(
1422    //                 MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
1423    //                     let button_style = if is_invite_pending {
1424    //                         &theme.disabled_button
1425    //                     } else {
1426    //                         theme.contact_button.style_for(mouse_state)
1427    //                     };
1428    //                     render_icon_button(button_style, "icons/x.svg").aligned()
1429    //                 })
1430    //                 .with_cursor_style(CursorStyle::PointingHand)
1431    //                 .on_click(MouseButton::Left, move |_, this, cx| {
1432    //                     this.respond_to_channel_invite(channel_id, false, cx);
1433    //                 })
1434    //                 .contained()
1435    //                 .with_margin_right(button_spacing),
1436    //             )
1437    //             .with_child(
1438    //                 MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
1439    //                     let button_style = if is_invite_pending {
1440    //                         &theme.disabled_button
1441    //                     } else {
1442    //                         theme.contact_button.style_for(mouse_state)
1443    //                     };
1444    //                     render_icon_button(button_style, "icons/check.svg")
1445    //                         .aligned()
1446    //                         .flex_float()
1447    //                 })
1448    //                 .with_cursor_style(CursorStyle::PointingHand)
1449    //                 .on_click(MouseButton::Left, move |_, this, cx| {
1450    //                     this.respond_to_channel_invite(channel_id, true, cx);
1451    //                 }),
1452    //             )
1453    //             .constrained()
1454    //             .with_height(theme.row_height)
1455    //             .contained()
1456    //             .with_style(
1457    //                 *theme
1458    //                     .contact_row
1459    //                     .in_state(is_selected)
1460    //                     .style_for(&mut Default::default()),
1461    //             )
1462    //             .with_padding_left(
1463    //                 theme.contact_row.default_style().padding.left + theme.channel_indent,
1464    //             )
1465    //             .into_any()
1466    //     }
1467
1468    fn has_subchannels(&self, ix: usize) -> bool {
1469        self.entries.get(ix).map_or(false, |entry| {
1470            if let ListEntry::Channel { has_children, .. } = entry {
1471                *has_children
1472            } else {
1473                false
1474            }
1475        })
1476    }
1477
1478    fn deploy_channel_context_menu(
1479        &mut self,
1480        position: Point<Pixels>,
1481        channel_id: ChannelId,
1482        ix: usize,
1483        cx: &mut ViewContext<Self>,
1484    ) {
1485        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
1486            self.channel_store
1487                .read(cx)
1488                .channel_for_id(clipboard.channel_id)
1489                .map(|channel| channel.name.clone())
1490        });
1491        let this = cx.view().clone();
1492
1493        let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
1494            if self.has_subchannels(ix) {
1495                let expand_action_name = if self.is_channel_collapsed(channel_id) {
1496                    "Expand Subchannels"
1497                } else {
1498                    "Collapse Subchannels"
1499                };
1500                context_menu = context_menu.entry(
1501                    expand_action_name,
1502                    cx.handler_for(&this, move |this, cx| {
1503                        this.toggle_channel_collapsed(channel_id, cx)
1504                    }),
1505                );
1506            }
1507
1508            context_menu = context_menu
1509                .entry(
1510                    "Open Notes",
1511                    cx.handler_for(&this, move |this, cx| {
1512                        this.open_channel_notes(channel_id, cx)
1513                    }),
1514                )
1515                .entry(
1516                    "Open Chat",
1517                    cx.handler_for(&this, move |this, cx| {
1518                        this.join_channel_chat(channel_id, cx)
1519                    }),
1520                )
1521                .entry(
1522                    "Copy Channel Link",
1523                    cx.handler_for(&this, move |this, cx| {
1524                        this.copy_channel_link(channel_id, cx)
1525                    }),
1526                );
1527
1528            if self.channel_store.read(cx).is_channel_admin(channel_id) {
1529                context_menu = context_menu
1530                    .separator()
1531                    .entry(
1532                        "New Subchannel",
1533                        cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
1534                    )
1535                    .entry(
1536                        "Rename",
1537                        cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
1538                    )
1539                    .entry(
1540                        "Move this channel",
1541                        cx.handler_for(&this, move |this, cx| {
1542                            this.start_move_channel(channel_id, cx)
1543                        }),
1544                    );
1545
1546                if let Some(channel_name) = clipboard_channel_name {
1547                    context_menu = context_menu.separator().entry(
1548                        format!("Move '#{}' here", channel_name),
1549                        cx.handler_for(&this, move |this, cx| {
1550                            this.move_channel_on_clipboard(channel_id, cx)
1551                        }),
1552                    );
1553                }
1554
1555                context_menu = context_menu
1556                    .separator()
1557                    .entry(
1558                        "Invite Members",
1559                        cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
1560                    )
1561                    .entry(
1562                        "Manage Members",
1563                        cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
1564                    )
1565                    .entry(
1566                        "Delete",
1567                        cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
1568                    );
1569            }
1570
1571            context_menu
1572        });
1573
1574        cx.focus_view(&context_menu);
1575        let subscription =
1576            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1577                if this.context_menu.as_ref().is_some_and(|context_menu| {
1578                    context_menu.0.focus_handle(cx).contains_focused(cx)
1579                }) {
1580                    cx.focus_self();
1581                }
1582                this.context_menu.take();
1583                cx.notify();
1584            });
1585        self.context_menu = Some((context_menu, position, subscription));
1586
1587        cx.notify();
1588    }
1589
1590    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1591        if self.take_editing_state(cx) {
1592            cx.focus_view(&self.filter_editor);
1593        } else {
1594            self.filter_editor.update(cx, |editor, cx| {
1595                if editor.buffer().read(cx).len(cx) > 0 {
1596                    editor.set_text("", cx);
1597                }
1598            });
1599        }
1600
1601        self.update_entries(false, cx);
1602    }
1603
1604    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1605        let ix = self.selection.map_or(0, |ix| ix + 1);
1606        if ix < self.entries.len() {
1607            self.selection = Some(ix);
1608        }
1609
1610        if let Some(ix) = self.selection {
1611            self.scroll_handle.scroll_to_item(ix)
1612        }
1613        cx.notify();
1614    }
1615
1616    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1617        let ix = self.selection.take().unwrap_or(0);
1618        if ix > 0 {
1619            self.selection = Some(ix - 1);
1620        }
1621
1622        if let Some(ix) = self.selection {
1623            self.scroll_handle.scroll_to_item(ix)
1624        }
1625        cx.notify();
1626    }
1627
1628    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1629        if self.confirm_channel_edit(cx) {
1630            return;
1631        }
1632
1633        if let Some(selection) = self.selection {
1634            if let Some(entry) = self.entries.get(selection) {
1635                match entry {
1636                    ListEntry::Header(section) => match section {
1637                        Section::ActiveCall => Self::leave_call(cx),
1638                        Section::Channels => self.new_root_channel(cx),
1639                        Section::Contacts => self.toggle_contact_finder(cx),
1640                        Section::ContactRequests
1641                        | Section::Online
1642                        | Section::Offline
1643                        | Section::ChannelInvites => {
1644                            self.toggle_section_expanded(*section, cx);
1645                        }
1646                    },
1647                    ListEntry::Contact { contact, calling } => {
1648                        if contact.online && !contact.busy && !calling {
1649                            self.call(contact.user.id, cx);
1650                        }
1651                    }
1652                    // ListEntry::ParticipantProject {
1653                    //     project_id,
1654                    //     host_user_id,
1655                    //     ..
1656                    // } => {
1657                    //     if let Some(workspace) = self.workspace.upgrade(cx) {
1658                    //         let app_state = workspace.read(cx).app_state().clone();
1659                    //         workspace::join_remote_project(
1660                    //             *project_id,
1661                    //             *host_user_id,
1662                    //             app_state,
1663                    //             cx,
1664                    //         )
1665                    //         .detach_and_log_err(cx);
1666                    //     }
1667                    // }
1668                    // ListEntry::ParticipantScreen { peer_id, .. } => {
1669                    //     let Some(peer_id) = peer_id else {
1670                    //         return;
1671                    //     };
1672                    //     if let Some(workspace) = self.workspace.upgrade(cx) {
1673                    //         workspace.update(cx, |workspace, cx| {
1674                    //             workspace.open_shared_screen(*peer_id, cx)
1675                    //         });
1676                    //     }
1677                    // }
1678                    ListEntry::Channel { channel, .. } => {
1679                        let is_active = maybe!({
1680                            let call_channel = ActiveCall::global(cx)
1681                                .read(cx)
1682                                .room()?
1683                                .read(cx)
1684                                .channel_id()?;
1685
1686                            Some(call_channel == channel.id)
1687                        })
1688                        .unwrap_or(false);
1689                        if is_active {
1690                            self.open_channel_notes(channel.id, cx)
1691                        } else {
1692                            self.join_channel(channel.id, cx)
1693                        }
1694                    }
1695                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
1696                    _ => {}
1697                }
1698            }
1699        }
1700    }
1701
1702    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
1703        if self.channel_editing_state.is_some() {
1704            self.channel_name_editor.update(cx, |editor, cx| {
1705                editor.insert(" ", cx);
1706            });
1707        }
1708    }
1709
1710    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
1711        if let Some(editing_state) = &mut self.channel_editing_state {
1712            match editing_state {
1713                ChannelEditingState::Create {
1714                    location,
1715                    pending_name,
1716                    ..
1717                } => {
1718                    if pending_name.is_some() {
1719                        return false;
1720                    }
1721                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1722
1723                    *pending_name = Some(channel_name.clone());
1724
1725                    self.channel_store
1726                        .update(cx, |channel_store, cx| {
1727                            channel_store.create_channel(&channel_name, *location, cx)
1728                        })
1729                        .detach();
1730                    cx.notify();
1731                }
1732                ChannelEditingState::Rename {
1733                    location,
1734                    pending_name,
1735                } => {
1736                    if pending_name.is_some() {
1737                        return false;
1738                    }
1739                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1740                    *pending_name = Some(channel_name.clone());
1741
1742                    self.channel_store
1743                        .update(cx, |channel_store, cx| {
1744                            channel_store.rename(*location, &channel_name, cx)
1745                        })
1746                        .detach();
1747                    cx.notify();
1748                }
1749            }
1750            cx.focus_self();
1751            true
1752        } else {
1753            false
1754        }
1755    }
1756
1757    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1758        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1759            self.collapsed_sections.remove(ix);
1760        } else {
1761            self.collapsed_sections.push(section);
1762        }
1763        self.update_entries(false, cx);
1764    }
1765
1766    fn collapse_selected_channel(
1767        &mut self,
1768        _: &CollapseSelectedChannel,
1769        cx: &mut ViewContext<Self>,
1770    ) {
1771        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1772            return;
1773        };
1774
1775        if self.is_channel_collapsed(channel_id) {
1776            return;
1777        }
1778
1779        self.toggle_channel_collapsed(channel_id, cx);
1780    }
1781
1782    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
1783        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1784            return;
1785        };
1786
1787        if !self.is_channel_collapsed(id) {
1788            return;
1789        }
1790
1791        self.toggle_channel_collapsed(id, cx)
1792    }
1793
1794    //     fn toggle_channel_collapsed_action(
1795    //         &mut self,
1796    //         action: &ToggleCollapse,
1797    //         cx: &mut ViewContext<Self>,
1798    //     ) {
1799    //         self.toggle_channel_collapsed(action.location, cx);
1800    //     }
1801
1802    fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1803        match self.collapsed_channels.binary_search(&channel_id) {
1804            Ok(ix) => {
1805                self.collapsed_channels.remove(ix);
1806            }
1807            Err(ix) => {
1808                self.collapsed_channels.insert(ix, channel_id);
1809            }
1810        };
1811        self.serialize(cx);
1812        self.update_entries(true, cx);
1813        cx.notify();
1814        cx.focus_self();
1815    }
1816
1817    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1818        self.collapsed_channels.binary_search(&channel_id).is_ok()
1819    }
1820
1821    fn leave_call(cx: &mut ViewContext<Self>) {
1822        ActiveCall::global(cx)
1823            .update(cx, |call, cx| call.hang_up(cx))
1824            .detach_and_log_err(cx);
1825    }
1826
1827    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1828        if let Some(workspace) = self.workspace.upgrade() {
1829            workspace.update(cx, |workspace, cx| {
1830                workspace.toggle_modal(cx, |cx| {
1831                    let mut finder = ContactFinder::new(self.user_store.clone(), cx);
1832                    finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1833                    finder
1834                });
1835            });
1836        }
1837    }
1838
1839    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
1840        self.channel_editing_state = Some(ChannelEditingState::Create {
1841            location: None,
1842            pending_name: None,
1843        });
1844        self.update_entries(false, cx);
1845        self.select_channel_editor();
1846        cx.focus_view(&self.channel_name_editor);
1847        cx.notify();
1848    }
1849
1850    fn select_channel_editor(&mut self) {
1851        self.selection = self.entries.iter().position(|entry| match entry {
1852            ListEntry::ChannelEditor { .. } => true,
1853            _ => false,
1854        });
1855    }
1856
1857    fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1858        self.collapsed_channels
1859            .retain(|channel| *channel != channel_id);
1860        self.channel_editing_state = Some(ChannelEditingState::Create {
1861            location: Some(channel_id),
1862            pending_name: None,
1863        });
1864        self.update_entries(false, cx);
1865        self.select_channel_editor();
1866        cx.focus_view(&self.channel_name_editor);
1867        cx.notify();
1868    }
1869
1870    fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1871        self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
1872    }
1873
1874    fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1875        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
1876    }
1877
1878    fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
1879        if let Some(channel) = self.selected_channel() {
1880            self.remove_channel(channel.id, cx)
1881        }
1882    }
1883
1884    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
1885        if let Some(channel) = self.selected_channel() {
1886            self.rename_channel(channel.id, cx);
1887        }
1888    }
1889
1890    fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1891        let channel_store = self.channel_store.read(cx);
1892        if !channel_store.is_channel_admin(channel_id) {
1893            return;
1894        }
1895        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
1896            self.channel_editing_state = Some(ChannelEditingState::Rename {
1897                location: channel_id,
1898                pending_name: None,
1899            });
1900            self.channel_name_editor.update(cx, |editor, cx| {
1901                editor.set_text(channel.name.clone(), cx);
1902                editor.select_all(&Default::default(), cx);
1903            });
1904            cx.focus_view(&self.channel_name_editor);
1905            self.update_entries(false, cx);
1906            self.select_channel_editor();
1907        }
1908    }
1909
1910    fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1911        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
1912    }
1913
1914    fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1915        if let Some(channel) = self.selected_channel() {
1916            self.channel_clipboard = Some(ChannelMoveClipboard {
1917                channel_id: channel.id,
1918            })
1919        }
1920    }
1921
1922    fn move_channel_on_clipboard(
1923        &mut self,
1924        to_channel_id: ChannelId,
1925        cx: &mut ViewContext<CollabPanel>,
1926    ) {
1927        if let Some(clipboard) = self.channel_clipboard.take() {
1928            self.channel_store.update(cx, |channel_store, cx| {
1929                channel_store
1930                    .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
1931                    .detach_and_log_err(cx)
1932            })
1933        }
1934    }
1935
1936    fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1937        if let Some(workspace) = self.workspace.upgrade() {
1938            todo!();
1939            // ChannelView::open(action.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                                                    .tooltip(|cx| {
2623                                                        Tooltip::text("Open channel notes", cx)
2624                                                    }),
2625                                            ),
2626                                    ),
2627                            ),
2628                    )
2629                    .toggle(disclosed)
2630                    .on_toggle(
2631                        cx.listener(move |this, _, cx| {
2632                            this.toggle_channel_collapsed(channel_id, cx)
2633                        }),
2634                    )
2635                    .on_click(cx.listener(move |this, _, cx| {
2636                        if this.drag_target_channel == ChannelDragTarget::None {
2637                            if is_active {
2638                                this.open_channel_notes(channel_id, cx)
2639                            } else {
2640                                this.join_channel(channel_id, cx)
2641                            }
2642                        }
2643                    }))
2644                    .on_secondary_mouse_down(cx.listener(
2645                        move |this, event: &MouseDownEvent, cx| {
2646                            this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2647                        },
2648                    )),
2649            )
2650            .tooltip(|cx| Tooltip::text("Join channel", cx))
2651
2652        // let channel_id = channel.id;
2653        // let collab_theme = &theme.collab_panel;
2654        // let is_public = self
2655        //     .channel_store
2656        //     .read(cx)
2657        //     .channel_for_id(channel_id)
2658        //     .map(|channel| channel.visibility)
2659        //     == Some(proto::ChannelVisibility::Public);
2660        // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2661        // let disclosed =
2662        //     has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2663
2664        // enum ChannelCall {}
2665        // enum ChannelNote {}
2666        // enum NotesTooltip {}
2667        // enum ChatTooltip {}
2668        // enum ChannelTooltip {}
2669
2670        // let mut is_dragged_over = false;
2671        // if cx
2672        //     .global::<DragAndDrop<Workspace>>()
2673        //     .currently_dragged::<Channel>(cx.window())
2674        //     .is_some()
2675        //     && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
2676        // {
2677        //     is_dragged_over = true;
2678        // }
2679
2680        // let has_messages_notification = channel.unseen_message_id.is_some();
2681
2682        // MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
2683        //     let row_hovered = state.hovered();
2684
2685        //     let mut select_state = |interactive: &Interactive<ContainerStyle>| {
2686        //         if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
2687        //             interactive.clicked.as_ref().unwrap().clone()
2688        //         } else if state.hovered() || other_selected {
2689        //             interactive
2690        //                 .hovered
2691        //                 .as_ref()
2692        //                 .unwrap_or(&interactive.default)
2693        //                 .clone()
2694        //         } else {
2695        //             interactive.default.clone()
2696        //         }
2697        //     };
2698
2699        //     Flex::<Self>::row()
2700        //         .with_child(
2701        //             Svg::new(if is_public {
2702        //                 "icons/public.svg"
2703        //             } else {
2704        //                 "icons/hash.svg"
2705        //             })
2706        //             .with_color(collab_theme.channel_hash.color)
2707        //             .constrained()
2708        //             .with_width(collab_theme.channel_hash.width)
2709        //             .aligned()
2710        //             .left(),
2711        //         )
2712        //         .with_child({
2713        //             let style = collab_theme.channel_name.inactive_state();
2714        //             Flex::row()
2715        //                 .with_child(
2716        //                     Label::new(channel.name.clone(), style.text.clone())
2717        //                         .contained()
2718        //                         .with_style(style.container)
2719        //                         .aligned()
2720        //                         .left()
2721        //                         .with_tooltip::<ChannelTooltip>(
2722        //                             ix,
2723        //                             "Join channel",
2724        //                             None,
2725        //                             theme.tooltip.clone(),
2726        //                             cx,
2727        //                         ),
2728        //                 )
2729        //                 .with_children({
2730        //                     let participants =
2731        //                         self.channel_store.read(cx).channel_participants(channel_id);
2732
2733        //                     if !participants.is_empty() {
2734        //                         let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2735
2736        //                         let result = FacePile::new(collab_theme.face_overlap)
2737        //                             .with_children(
2738        //                                 participants
2739        //                                     .iter()
2740        //                                     .filter_map(|user| {
2741        //                                         Some(
2742        //                                             Image::from_data(user.avatar.clone()?)
2743        //                                                 .with_style(collab_theme.channel_avatar),
2744        //                                         )
2745        //                                     })
2746        //                                     .take(FACEPILE_LIMIT),
2747        //                             )
2748        //                             .with_children((extra_count > 0).then(|| {
2749        //                                 Label::new(
2750        //                                     format!("+{}", extra_count),
2751        //                                     collab_theme.extra_participant_label.text.clone(),
2752        //                                 )
2753        //                                 .contained()
2754        //                                 .with_style(collab_theme.extra_participant_label.container)
2755        //                             }));
2756
2757        //                         Some(result)
2758        //                     } else {
2759        //                         None
2760        //                     }
2761        //                 })
2762        //                 .with_spacing(8.)
2763        //                 .align_children_center()
2764        //                 .flex(1., true)
2765        //         })
2766        //         .with_child(
2767        //             MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
2768        //                 let container_style = collab_theme
2769        //                     .disclosure
2770        //                     .button
2771        //                     .style_for(mouse_state)
2772        //                     .container;
2773
2774        //                 if channel.unseen_message_id.is_some() {
2775        //                     Svg::new("icons/conversations.svg")
2776        //                         .with_color(collab_theme.channel_note_active_color)
2777        //                         .constrained()
2778        //                         .with_width(collab_theme.channel_hash.width)
2779        //                         .contained()
2780        //                         .with_style(container_style)
2781        //                         .with_uniform_padding(4.)
2782        //                         .into_any()
2783        //                 } else if row_hovered {
2784        //                     Svg::new("icons/conversations.svg")
2785        //                         .with_color(collab_theme.channel_hash.color)
2786        //                         .constrained()
2787        //                         .with_width(collab_theme.channel_hash.width)
2788        //                         .contained()
2789        //                         .with_style(container_style)
2790        //                         .with_uniform_padding(4.)
2791        //                         .into_any()
2792        //                 } else {
2793        //                     Empty::new().into_any()
2794        //                 }
2795        //             })
2796        //             .on_click(MouseButton::Left, move |_, this, cx| {
2797        //                 this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
2798        //             })
2799        //             .with_tooltip::<ChatTooltip>(
2800        //                 ix,
2801        //                 "Open channel chat",
2802        //                 None,
2803        //                 theme.tooltip.clone(),
2804        //                 cx,
2805        //             )
2806        //             .contained()
2807        //             .with_margin_right(4.),
2808        //         )
2809        //         .with_child(
2810        //             MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
2811        //                 let container_style = collab_theme
2812        //                     .disclosure
2813        //                     .button
2814        //                     .style_for(mouse_state)
2815        //                     .container;
2816        //                 if row_hovered || channel.unseen_note_version.is_some() {
2817        //                     Svg::new("icons/file.svg")
2818        //                         .with_color(if channel.unseen_note_version.is_some() {
2819        //                             collab_theme.channel_note_active_color
2820        //                         } else {
2821        //                             collab_theme.channel_hash.color
2822        //                         })
2823        //                         .constrained()
2824        //                         .with_width(collab_theme.channel_hash.width)
2825        //                         .contained()
2826        //                         .with_style(container_style)
2827        //                         .with_uniform_padding(4.)
2828        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2829        //                         .with_tooltip::<NotesTooltip>(
2830        //                             ix as usize,
2831        //                             "Open channel notes",
2832        //                             None,
2833        //                             theme.tooltip.clone(),
2834        //                             cx,
2835        //                         )
2836        //                         .into_any()
2837        //                 } else if has_messages_notification {
2838        //                     Empty::new()
2839        //                         .constrained()
2840        //                         .with_width(collab_theme.channel_hash.width)
2841        //                         .contained()
2842        //                         .with_uniform_padding(4.)
2843        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2844        //                         .into_any()
2845        //                 } else {
2846        //                     Empty::new().into_any()
2847        //                 }
2848        //             })
2849        //             .on_click(MouseButton::Left, move |_, this, cx| {
2850        //                 this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2851        //             }),
2852        //         )
2853        //         .align_children_center()
2854        //         .styleable_component()
2855        //         .disclosable(
2856        //             disclosed,
2857        //             Box::new(ToggleCollapse {
2858        //                 location: channel.id.clone(),
2859        //             }),
2860        //         )
2861        //         .with_id(ix)
2862        //         .with_style(collab_theme.disclosure.clone())
2863        //         .element()
2864        //         .constrained()
2865        //         .with_height(collab_theme.row_height)
2866        //         .contained()
2867        //         .with_style(select_state(
2868        //             collab_theme
2869        //                 .channel_row
2870        //                 .in_state(is_selected || is_active || is_dragged_over),
2871        //         ))
2872        //         .with_padding_left(
2873        //             collab_theme.channel_row.default_style().padding.left
2874        //                 + collab_theme.channel_indent * depth as f32,
2875        //         )
2876        // })
2877        // .on_click(MouseButton::Left, move |_, this, cx| {
2878        //     if this.
2879        // drag_target_channel == ChannelDragTarget::None {
2880        //         if is_active {
2881        //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
2882        //         } else {
2883        //             this.join_channel(channel_id, cx)
2884        //         }
2885        //     }
2886        // })
2887        // .on_click(MouseButton::Right, {
2888        //     let channel = channel.clone();
2889        //     move |e, this, cx| {
2890        //         this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
2891        //     }
2892        // })
2893        // .on_up(MouseButton::Left, move |_, this, cx| {
2894        //     if let Some((_, dragged_channel)) = cx
2895        //         .global::<DragAndDrop<Workspace>>()
2896        //         .currently_dragged::<Channel>(cx.window())
2897        //     {
2898        //         this.channel_store
2899        //             .update(cx, |channel_store, cx| {
2900        //                 channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2901        //             })
2902        //             .detach_and_log_err(cx)
2903        //     }
2904        // })
2905        // .on_move({
2906        //     let channel = channel.clone();
2907        //     move |_, this, cx| {
2908        //         if let Some((_, dragged_channel)) = cx
2909        //             .global::<DragAndDrop<Workspace>>()
2910        //             .currently_dragged::<Channel>(cx.window())
2911        //         {
2912        //             if channel.id != dragged_channel.id {
2913        //                 this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
2914        //             }
2915        //             cx.notify()
2916        //         }
2917        //     }
2918        // })
2919        // .as_draggable::<_, Channel>(
2920        //     channel.clone(),
2921        //     move |_, channel, cx: &mut ViewContext<Workspace>| {
2922        //         let theme = &theme::current(cx).collab_panel;
2923
2924        //         Flex::<Workspace>::row()
2925        //             .with_child(
2926        //                 Svg::new("icons/hash.svg")
2927        //                     .with_color(theme.channel_hash.color)
2928        //                     .constrained()
2929        //                     .with_width(theme.channel_hash.width)
2930        //                     .aligned()
2931        //                     .left(),
2932        //             )
2933        //             .with_child(
2934        //                 Label::new(channel.name.clone(), theme.channel_name.text.clone())
2935        //                     .contained()
2936        //                     .with_style(theme.channel_name.container)
2937        //                     .aligned()
2938        //                     .left(),
2939        //             )
2940        //             .align_children_center()
2941        //             .contained()
2942        //             .with_background_color(
2943        //                 theme
2944        //                     .container
2945        //                     .background_color
2946        //                     .unwrap_or(gpui::color::Color::transparent_black()),
2947        //             )
2948        //             .contained()
2949        //             .with_padding_left(
2950        //                 theme.channel_row.default_style().padding.left
2951        //                     + theme.channel_indent * depth as f32,
2952        //             )
2953        //             .into_any()
2954        //     },
2955        // )
2956        // .with_cursor_style(CursorStyle::PointingHand)
2957        // .into_any()
2958    }
2959
2960    fn render_channel_editor(
2961        &mut self,
2962        depth: usize,
2963        cx: &mut ViewContext<Self>,
2964    ) -> impl IntoElement {
2965        let item = ListItem::new("channel-editor")
2966            .inset(false)
2967            .indent_level(depth)
2968            .left_icon(Icon::Hash);
2969
2970        if let Some(pending_name) = self
2971            .channel_editing_state
2972            .as_ref()
2973            .and_then(|state| state.pending_name())
2974        {
2975            item.child(Label::new(pending_name))
2976        } else {
2977            item.child(
2978                div()
2979                    .w_full()
2980                    .py_1() // todo!() @nate this is a px off at the default font size.
2981                    .child(self.channel_name_editor.clone()),
2982            )
2983        }
2984    }
2985}
2986
2987fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
2988    let rem_size = cx.rem_size();
2989    let line_height = cx.text_style().line_height_in_pixels(rem_size);
2990    let width = rem_size * 1.5;
2991    let thickness = px(2.);
2992    let color = cx.theme().colors().text;
2993
2994    canvas(move |bounds, cx| {
2995        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2996        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2997        let right = bounds.right();
2998        let top = bounds.top();
2999
3000        cx.paint_quad(
3001            Bounds::from_corners(
3002                point(start_x, top),
3003                point(
3004                    start_x + thickness,
3005                    if is_last { start_y } else { bounds.bottom() },
3006                ),
3007            ),
3008            Default::default(),
3009            color,
3010            Default::default(),
3011            Hsla::transparent_black(),
3012        );
3013        cx.paint_quad(
3014            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
3015            Default::default(),
3016            color,
3017            Default::default(),
3018            Hsla::transparent_black(),
3019        );
3020    })
3021    .w(width)
3022    .h(line_height)
3023}
3024
3025impl Render for CollabPanel {
3026    type Element = Focusable<Div>;
3027
3028    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3029        v_stack()
3030            .key_context("CollabPanel")
3031            .on_action(cx.listener(CollabPanel::cancel))
3032            .on_action(cx.listener(CollabPanel::select_next))
3033            .on_action(cx.listener(CollabPanel::select_prev))
3034            .on_action(cx.listener(CollabPanel::confirm))
3035            .on_action(cx.listener(CollabPanel::insert_space))
3036            //     .on_action(cx.listener(CollabPanel::remove))
3037            .on_action(cx.listener(CollabPanel::remove_selected_channel))
3038            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
3039            //     .on_action(cx.listener(CollabPanel::new_subchannel))
3040            //     .on_action(cx.listener(CollabPanel::invite_members))
3041            //     .on_action(cx.listener(CollabPanel::manage_members))
3042            .on_action(cx.listener(CollabPanel::rename_selected_channel))
3043            //     .on_action(cx.listener(CollabPanel::rename_channel))
3044            //     .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action))
3045            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
3046            .on_action(cx.listener(CollabPanel::expand_selected_channel))
3047            //     .on_action(cx.listener(CollabPanel::open_channel_notes))
3048            //     .on_action(cx.listener(CollabPanel::join_channel_chat))
3049            //     .on_action(cx.listener(CollabPanel::copy_channel_link))
3050            .track_focus(&self.focus_handle)
3051            .size_full()
3052            .child(if self.user_store.read(cx).current_user().is_none() {
3053                self.render_signed_out(cx)
3054            } else {
3055                self.render_signed_in(cx)
3056            })
3057            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3058                overlay()
3059                    .position(*position)
3060                    .anchor(gpui::AnchorCorner::TopLeft)
3061                    .child(menu.clone())
3062            }))
3063    }
3064}
3065
3066// impl View for CollabPanel {
3067//     fn ui_name() -> &'static str {
3068//         "CollabPanel"
3069//     }
3070
3071//     fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
3072//         if !self.has_focus {
3073//             self.has_focus = true;
3074//             if !self.context_menu.is_focused(cx) {
3075//                 if let Some(editing_state) = &self.channel_editing_state {
3076//                     if editing_state.pending_name().is_none() {
3077//                         cx.focus(&self.channel_name_editor);
3078//                     } else {
3079//                         cx.focus(&self.filter_editor);
3080//                     }
3081//                 } else {
3082//                     cx.focus(&self.filter_editor);
3083//                 }
3084//             }
3085//             cx.emit(Event::Focus);
3086//         }
3087//     }
3088
3089//     fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
3090//         self.has_focus = false;
3091//     }
3092
3093//     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
3094//         let theme = &theme::current(cx).collab_panel;
3095
3096//         if self.user_store.read(cx).current_user().is_none() {
3097//             enum LogInButton {}
3098
3099//             return Flex::column()
3100//                 .with_child(
3101//                     MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
3102//                         let button = theme.log_in_button.style_for(state);
3103//                         Label::new("Sign in to collaborate", button.text.clone())
3104//                             .aligned()
3105//                             .left()
3106//                             .contained()
3107//                             .with_style(button.container)
3108//                     })
3109//                     .on_click(MouseButton::Left, |_, this, cx| {
3110//                         let client = this.client.clone();
3111//                         cx.spawn(|_, cx| async move {
3112//                             client.authenticate_and_connect(true, &cx).await.log_err();
3113//                         })
3114//                         .detach();
3115//                     })
3116//                     .with_cursor_style(CursorStyle::PointingHand),
3117//                 )
3118//                 .contained()
3119//                 .with_style(theme.container)
3120//                 .into_any();
3121//         }
3122
3123//         enum PanelFocus {}
3124//         MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
3125//             Stack::new()
3126//                 .with_child(
3127//                     Flex::column()
3128//                         .with_child(
3129//                             Flex::row().with_child(
3130//                                 ChildView::new(&self.filter_editor, cx)
3131//                                     .contained()
3132//                                     .with_style(theme.user_query_editor.container)
3133//                                     .flex(1.0, true),
3134//                             ),
3135//                         )
3136//                         .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3137//                         .contained()
3138//                         .with_style(theme.container)
3139//                         .into_any(),
3140//                 )
3141//                 .with_children(
3142//                     (!self.context_menu_on_selected)
3143//                         .then(|| ChildView::new(&self.context_menu, cx)),
3144//                 )
3145//                 .into_any()
3146//         })
3147//         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3148//         .into_any_named("collab panel")
3149//     }
3150
3151//     fn update_keymap_context(
3152//         &self,
3153//         keymap: &mut gpui::keymap_matcher::KeymapContext,
3154//         _: &AppContext,
3155//     ) {
3156//         Self::reset_to_default_keymap_context(keymap);
3157//         if self.channel_editing_state.is_some() {
3158//             keymap.add_identifier("editing");
3159//         } else {
3160//             keymap.add_identifier("not_editing");
3161//         }
3162//     }
3163// }
3164
3165impl EventEmitter<PanelEvent> for CollabPanel {}
3166
3167impl Panel for CollabPanel {
3168    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3169        CollaborationPanelSettings::get_global(cx).dock
3170    }
3171
3172    fn position_is_valid(&self, position: DockPosition) -> bool {
3173        matches!(position, DockPosition::Left | DockPosition::Right)
3174    }
3175
3176    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3177        settings::update_settings_file::<CollaborationPanelSettings>(
3178            self.fs.clone(),
3179            cx,
3180            move |settings| settings.dock = Some(position),
3181        );
3182    }
3183
3184    fn size(&self, cx: &gpui::WindowContext) -> f32 {
3185        self.width.map_or_else(
3186            || CollaborationPanelSettings::get_global(cx).default_width,
3187            |width| width.0,
3188        )
3189    }
3190
3191    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3192        self.width = size.map(|s| px(s));
3193        self.serialize(cx);
3194        cx.notify();
3195    }
3196
3197    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
3198        CollaborationPanelSettings::get_global(cx)
3199            .button
3200            .then(|| ui::Icon::Collab)
3201    }
3202
3203    fn toggle_action(&self) -> Box<dyn gpui::Action> {
3204        Box::new(ToggleFocus)
3205    }
3206
3207    fn has_focus(&self, cx: &gpui::WindowContext) -> bool {
3208        self.focus_handle.contains_focused(cx)
3209    }
3210
3211    fn persistent_name() -> &'static str {
3212        "CollabPanel"
3213    }
3214}
3215
3216impl FocusableView for CollabPanel {
3217    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
3218        self.filter_editor.focus_handle(cx).clone()
3219    }
3220}
3221
3222impl PartialEq for ListEntry {
3223    fn eq(&self, other: &Self) -> bool {
3224        match self {
3225            ListEntry::Header(section_1) => {
3226                if let ListEntry::Header(section_2) = other {
3227                    return section_1 == section_2;
3228                }
3229            }
3230            ListEntry::CallParticipant { user: user_1, .. } => {
3231                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3232                    return user_1.id == user_2.id;
3233                }
3234            }
3235            ListEntry::ParticipantProject {
3236                project_id: project_id_1,
3237                ..
3238            } => {
3239                if let ListEntry::ParticipantProject {
3240                    project_id: project_id_2,
3241                    ..
3242                } = other
3243                {
3244                    return project_id_1 == project_id_2;
3245                }
3246            }
3247            ListEntry::ParticipantScreen {
3248                peer_id: peer_id_1, ..
3249            } => {
3250                if let ListEntry::ParticipantScreen {
3251                    peer_id: peer_id_2, ..
3252                } = other
3253                {
3254                    return peer_id_1 == peer_id_2;
3255                }
3256            }
3257            ListEntry::Channel {
3258                channel: channel_1, ..
3259            } => {
3260                if let ListEntry::Channel {
3261                    channel: channel_2, ..
3262                } = other
3263                {
3264                    return channel_1.id == channel_2.id;
3265                }
3266            }
3267            ListEntry::ChannelNotes { channel_id } => {
3268                if let ListEntry::ChannelNotes {
3269                    channel_id: other_id,
3270                } = other
3271                {
3272                    return channel_id == other_id;
3273                }
3274            }
3275            ListEntry::ChannelChat { channel_id } => {
3276                if let ListEntry::ChannelChat {
3277                    channel_id: other_id,
3278                } = other
3279                {
3280                    return channel_id == other_id;
3281                }
3282            }
3283            // ListEntry::ChannelInvite(channel_1) => {
3284            //     if let ListEntry::ChannelInvite(channel_2) = other {
3285            //         return channel_1.id == channel_2.id;
3286            //     }
3287            // }
3288            ListEntry::IncomingRequest(user_1) => {
3289                if let ListEntry::IncomingRequest(user_2) = other {
3290                    return user_1.id == user_2.id;
3291                }
3292            }
3293            ListEntry::OutgoingRequest(user_1) => {
3294                if let ListEntry::OutgoingRequest(user_2) = other {
3295                    return user_1.id == user_2.id;
3296                }
3297            }
3298            ListEntry::Contact {
3299                contact: contact_1, ..
3300            } => {
3301                if let ListEntry::Contact {
3302                    contact: contact_2, ..
3303                } = other
3304                {
3305                    return contact_1.user.id == contact_2.user.id;
3306                }
3307            }
3308            ListEntry::ChannelEditor { depth } => {
3309                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3310                    return depth == other_depth;
3311                }
3312            }
3313            ListEntry::ContactPlaceholder => {
3314                if let ListEntry::ContactPlaceholder = other {
3315                    return true;
3316                }
3317            }
3318        }
3319        false
3320    }
3321}
3322
3323// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3324//     Svg::new(svg_path)
3325//         .with_color(style.color)
3326//         .constrained()
3327//         .with_width(style.icon_width)
3328//         .aligned()
3329//         .constrained()
3330//         .with_width(style.button_width)
3331//         .with_height(style.button_width)
3332//         .contained()
3333//         .with_style(style.container)
3334// }
3335
3336struct DraggedChannelView {
3337    channel: Channel,
3338    width: Pixels,
3339}
3340
3341impl Render for DraggedChannelView {
3342    type Element = Div;
3343
3344    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3345        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3346        h_stack()
3347            .font(ui_font)
3348            .bg(cx.theme().colors().background)
3349            .w(self.width)
3350            .p_1()
3351            .gap_1()
3352            .child(
3353                IconElement::new(
3354                    if self.channel.visibility == proto::ChannelVisibility::Public {
3355                        Icon::Public
3356                    } else {
3357                        Icon::Hash
3358                    },
3359                )
3360                .size(IconSize::Small)
3361                .color(Color::Muted),
3362            )
3363            .child(Label::new(self.channel.name.clone()))
3364    }
3365}