collab_panel.rs

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