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, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
 179    serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem,
 180    DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla,
 181    InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point,
 182    PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled,
 183    Subscription, Task, View, 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...", 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            .start_slot(Avatar::new(user.avatar_uri.clone()))
1160            .child(Label::new(user.github_login.clone()))
1161            .end_slot(if is_pending {
1162                Label::new("Calling").color(Color::Muted).into_any_element()
1163            } else if is_current_user {
1164                IconButton::new("leave-call", Icon::Exit)
1165                    .style(ButtonStyle::Subtle)
1166                    .on_click(cx.listener(move |this, _, cx| {
1167                        Self::leave_call(cx);
1168                    }))
1169                    .tooltip(|cx| Tooltip::text("Leave Call", cx))
1170                    .into_any_element()
1171            } else {
1172                div().into_any_element()
1173            })
1174            .when_some(peer_id, |this, peer_id| {
1175                this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
1176                    .on_click(cx.listener(move |this, _, cx| {
1177                        this.workspace
1178                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx));
1179                    }))
1180            })
1181    }
1182
1183    fn render_participant_project(
1184        &self,
1185        project_id: u64,
1186        worktree_root_names: &[String],
1187        host_user_id: u64,
1188        //         is_current: bool,
1189        is_last: bool,
1190        //         is_selected: bool,
1191        //         theme: &theme::Theme,
1192        cx: &mut ViewContext<Self>,
1193    ) -> impl IntoElement {
1194        let project_name: SharedString = if worktree_root_names.is_empty() {
1195            "untitled".to_string()
1196        } else {
1197            worktree_root_names.join(", ")
1198        }
1199        .into();
1200
1201        let theme = cx.theme();
1202
1203        ListItem::new(project_id as usize)
1204            .on_click(cx.listener(move |this, _, cx| {
1205                this.workspace.update(cx, |workspace, cx| {
1206                    let app_state = workspace.app_state().clone();
1207                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1208                        .detach_and_log_err(cx);
1209                });
1210            }))
1211            .start_slot(
1212                h_stack()
1213                    .gap_1()
1214                    .child(render_tree_branch(is_last, cx))
1215                    .child(IconButton::new(0, Icon::Folder)),
1216            )
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            .start_slot(
1309                h_stack()
1310                    .gap_1()
1311                    .child(render_tree_branch(is_last, cx))
1312                    .child(IconButton::new(0, Icon::Screen)),
1313            )
1314            .child(Label::new("Screen"))
1315            .when_some(peer_id, |this, _| {
1316                this.on_click(cx.listener(move |this, _, cx| {
1317                    this.workspace.update(cx, |workspace, cx| {
1318                        workspace.open_shared_screen(peer_id.unwrap(), cx)
1319                    });
1320                }))
1321                .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
1322            })
1323    }
1324
1325    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1326        if let Some(_) = self.channel_editing_state.take() {
1327            self.channel_name_editor.update(cx, |editor, cx| {
1328                editor.set_text("", cx);
1329            });
1330            true
1331        } else {
1332            false
1333        }
1334    }
1335
1336    //     fn render_contact_placeholder(
1337    //         &self,
1338    //         theme: &theme::CollabPanel,
1339    //         is_selected: bool,
1340    //         cx: &mut ViewContext<Self>,
1341    //     ) -> AnyElement<Self> {
1342    //         enum AddContacts {}
1343    //         MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1344    //             let style = theme.list_empty_state.style_for(is_selected, state);
1345    //             Flex::row()
1346    //                 .with_child(
1347    //                     Svg::new("icons/plus.svg")
1348    //                         .with_color(theme.list_empty_icon.color)
1349    //                         .constrained()
1350    //                         .with_width(theme.list_empty_icon.width)
1351    //                         .aligned()
1352    //                         .left(),
1353    //                 )
1354    //                 .with_child(
1355    //                     Label::new("Add a contact", style.text.clone())
1356    //                         .contained()
1357    //                         .with_style(theme.list_empty_label_container),
1358    //                 )
1359    //                 .align_children_center()
1360    //                 .contained()
1361    //                 .with_style(style.container)
1362    //                 .into_any()
1363    //         })
1364    //         .on_click(MouseButton::Left, |_, this, cx| {
1365    //             this.toggle_contact_finder(cx);
1366    //         })
1367    //         .into_any()
1368    //     }
1369
1370    fn render_channel_notes(
1371        &self,
1372        channel_id: ChannelId,
1373        cx: &mut ViewContext<Self>,
1374    ) -> impl IntoElement {
1375        ListItem::new("channel-notes")
1376            .on_click(cx.listener(move |this, _, cx| {
1377                this.open_channel_notes(channel_id, cx);
1378            }))
1379            .start_slot(
1380                h_stack()
1381                    .gap_1()
1382                    .child(render_tree_branch(false, cx))
1383                    .child(IconButton::new(0, Icon::File)),
1384            )
1385            .child(div().h_7().w_full().child(Label::new("notes")))
1386            .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
1387    }
1388
1389    fn render_channel_chat(
1390        &self,
1391        channel_id: ChannelId,
1392        cx: &mut ViewContext<Self>,
1393    ) -> impl IntoElement {
1394        ListItem::new("channel-chat")
1395            .on_click(cx.listener(move |this, _, cx| {
1396                this.join_channel_chat(channel_id, cx);
1397            }))
1398            .start_slot(
1399                h_stack()
1400                    .gap_1()
1401                    .child(render_tree_branch(false, cx))
1402                    .child(IconButton::new(0, Icon::MessageBubbles)),
1403            )
1404            .child(Label::new("chat"))
1405            .tooltip(move |cx| Tooltip::text("Open Chat", cx))
1406    }
1407
1408    //     fn render_channel_invite(
1409    //         channel: Arc<Channel>,
1410    //         channel_store: ModelHandle<ChannelStore>,
1411    //         theme: &theme::CollabPanel,
1412    //         is_selected: bool,
1413    //         cx: &mut ViewContext<Self>,
1414    //     ) -> AnyElement<Self> {
1415    //         enum Decline {}
1416    //         enum Accept {}
1417
1418    //         let channel_id = channel.id;
1419    //         let is_invite_pending = channel_store
1420    //             .read(cx)
1421    //             .has_pending_channel_invite_response(&channel);
1422    //         let button_spacing = theme.contact_button_spacing;
1423
1424    //         Flex::row()
1425    //             .with_child(
1426    //                 Svg::new("icons/hash.svg")
1427    //                     .with_color(theme.channel_hash.color)
1428    //                     .constrained()
1429    //                     .with_width(theme.channel_hash.width)
1430    //                     .aligned()
1431    //                     .left(),
1432    //             )
1433    //             .with_child(
1434    //                 Label::new(channel.name.clone(), theme.contact_username.text.clone())
1435    //                     .contained()
1436    //                     .with_style(theme.contact_username.container)
1437    //                     .aligned()
1438    //                     .left()
1439    //                     .flex(1., true),
1440    //             )
1441    //             .with_child(
1442    //                 MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
1443    //                     let button_style = if is_invite_pending {
1444    //                         &theme.disabled_button
1445    //                     } else {
1446    //                         theme.contact_button.style_for(mouse_state)
1447    //                     };
1448    //                     render_icon_button(button_style, "icons/x.svg").aligned()
1449    //                 })
1450    //                 .with_cursor_style(CursorStyle::PointingHand)
1451    //                 .on_click(MouseButton::Left, move |_, this, cx| {
1452    //                     this.respond_to_channel_invite(channel_id, false, cx);
1453    //                 })
1454    //                 .contained()
1455    //                 .with_margin_right(button_spacing),
1456    //             )
1457    //             .with_child(
1458    //                 MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
1459    //                     let button_style = if is_invite_pending {
1460    //                         &theme.disabled_button
1461    //                     } else {
1462    //                         theme.contact_button.style_for(mouse_state)
1463    //                     };
1464    //                     render_icon_button(button_style, "icons/check.svg")
1465    //                         .aligned()
1466    //                         .flex_float()
1467    //                 })
1468    //                 .with_cursor_style(CursorStyle::PointingHand)
1469    //                 .on_click(MouseButton::Left, move |_, this, cx| {
1470    //                     this.respond_to_channel_invite(channel_id, true, cx);
1471    //                 }),
1472    //             )
1473    //             .constrained()
1474    //             .with_height(theme.row_height)
1475    //             .contained()
1476    //             .with_style(
1477    //                 *theme
1478    //                     .contact_row
1479    //                     .in_state(is_selected)
1480    //                     .style_for(&mut Default::default()),
1481    //             )
1482    //             .with_padding_left(
1483    //                 theme.contact_row.default_style().padding.left + theme.channel_indent,
1484    //             )
1485    //             .into_any()
1486    //     }
1487
1488    fn has_subchannels(&self, ix: usize) -> bool {
1489        self.entries.get(ix).map_or(false, |entry| {
1490            if let ListEntry::Channel { has_children, .. } = entry {
1491                *has_children
1492            } else {
1493                false
1494            }
1495        })
1496    }
1497
1498    fn deploy_channel_context_menu(
1499        &mut self,
1500        position: Point<Pixels>,
1501        channel_id: ChannelId,
1502        ix: usize,
1503        cx: &mut ViewContext<Self>,
1504    ) {
1505        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
1506            self.channel_store
1507                .read(cx)
1508                .channel_for_id(clipboard.channel_id)
1509                .map(|channel| channel.name.clone())
1510        });
1511        let this = cx.view().clone();
1512
1513        let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
1514            if self.has_subchannels(ix) {
1515                let expand_action_name = if self.is_channel_collapsed(channel_id) {
1516                    "Expand Subchannels"
1517                } else {
1518                    "Collapse Subchannels"
1519                };
1520                context_menu = context_menu.entry(
1521                    expand_action_name,
1522                    cx.handler_for(&this, move |this, cx| {
1523                        this.toggle_channel_collapsed(channel_id, cx)
1524                    }),
1525                );
1526            }
1527
1528            context_menu = context_menu
1529                .entry(
1530                    "Open Notes",
1531                    cx.handler_for(&this, move |this, cx| {
1532                        this.open_channel_notes(channel_id, cx)
1533                    }),
1534                )
1535                .entry(
1536                    "Open Chat",
1537                    cx.handler_for(&this, move |this, cx| {
1538                        this.join_channel_chat(channel_id, cx)
1539                    }),
1540                )
1541                .entry(
1542                    "Copy Channel Link",
1543                    cx.handler_for(&this, move |this, cx| {
1544                        this.copy_channel_link(channel_id, cx)
1545                    }),
1546                );
1547
1548            if self.channel_store.read(cx).is_channel_admin(channel_id) {
1549                context_menu = context_menu
1550                    .separator()
1551                    .entry(
1552                        "New Subchannel",
1553                        cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
1554                    )
1555                    .entry(
1556                        "Rename",
1557                        cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
1558                    )
1559                    .entry(
1560                        "Move this channel",
1561                        cx.handler_for(&this, move |this, cx| {
1562                            this.start_move_channel(channel_id, cx)
1563                        }),
1564                    );
1565
1566                if let Some(channel_name) = clipboard_channel_name {
1567                    context_menu = context_menu.separator().entry(
1568                        format!("Move '#{}' here", channel_name),
1569                        cx.handler_for(&this, move |this, cx| {
1570                            this.move_channel_on_clipboard(channel_id, cx)
1571                        }),
1572                    );
1573                }
1574
1575                context_menu = context_menu
1576                    .separator()
1577                    .entry(
1578                        "Invite Members",
1579                        cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
1580                    )
1581                    .entry(
1582                        "Manage Members",
1583                        cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
1584                    )
1585                    .entry(
1586                        "Delete",
1587                        cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
1588                    );
1589            }
1590
1591            context_menu
1592        });
1593
1594        cx.focus_view(&context_menu);
1595        let subscription =
1596            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1597                if this.context_menu.as_ref().is_some_and(|context_menu| {
1598                    context_menu.0.focus_handle(cx).contains_focused(cx)
1599                }) {
1600                    cx.focus_self();
1601                }
1602                this.context_menu.take();
1603                cx.notify();
1604            });
1605        self.context_menu = Some((context_menu, position, subscription));
1606
1607        cx.notify();
1608    }
1609
1610    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1611        if self.take_editing_state(cx) {
1612            cx.focus_view(&self.filter_editor);
1613        } else {
1614            self.filter_editor.update(cx, |editor, cx| {
1615                if editor.buffer().read(cx).len(cx) > 0 {
1616                    editor.set_text("", cx);
1617                }
1618            });
1619        }
1620
1621        self.update_entries(false, cx);
1622    }
1623
1624    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1625        let ix = self.selection.map_or(0, |ix| ix + 1);
1626        if ix < self.entries.len() {
1627            self.selection = Some(ix);
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 select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1637        let ix = self.selection.take().unwrap_or(0);
1638        if ix > 0 {
1639            self.selection = Some(ix - 1);
1640        }
1641
1642        if let Some(ix) = self.selection {
1643            self.scroll_handle.scroll_to_item(ix)
1644        }
1645        cx.notify();
1646    }
1647
1648    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1649        if self.confirm_channel_edit(cx) {
1650            return;
1651        }
1652
1653        if let Some(selection) = self.selection {
1654            if let Some(entry) = self.entries.get(selection) {
1655                match entry {
1656                    ListEntry::Header(section) => match section {
1657                        Section::ActiveCall => Self::leave_call(cx),
1658                        Section::Channels => self.new_root_channel(cx),
1659                        Section::Contacts => self.toggle_contact_finder(cx),
1660                        Section::ContactRequests
1661                        | Section::Online
1662                        | Section::Offline
1663                        | Section::ChannelInvites => {
1664                            self.toggle_section_expanded(*section, cx);
1665                        }
1666                    },
1667                    ListEntry::Contact { contact, calling } => {
1668                        if contact.online && !contact.busy && !calling {
1669                            self.call(contact.user.id, cx);
1670                        }
1671                    }
1672                    // ListEntry::ParticipantProject {
1673                    //     project_id,
1674                    //     host_user_id,
1675                    //     ..
1676                    // } => {
1677                    //     if let Some(workspace) = self.workspace.upgrade(cx) {
1678                    //         let app_state = workspace.read(cx).app_state().clone();
1679                    //         workspace::join_remote_project(
1680                    //             *project_id,
1681                    //             *host_user_id,
1682                    //             app_state,
1683                    //             cx,
1684                    //         )
1685                    //         .detach_and_log_err(cx);
1686                    //     }
1687                    // }
1688                    // ListEntry::ParticipantScreen { peer_id, .. } => {
1689                    //     let Some(peer_id) = peer_id else {
1690                    //         return;
1691                    //     };
1692                    //     if let Some(workspace) = self.workspace.upgrade(cx) {
1693                    //         workspace.update(cx, |workspace, cx| {
1694                    //             workspace.open_shared_screen(*peer_id, cx)
1695                    //         });
1696                    //     }
1697                    // }
1698                    ListEntry::Channel { channel, .. } => {
1699                        let is_active = maybe!({
1700                            let call_channel = ActiveCall::global(cx)
1701                                .read(cx)
1702                                .room()?
1703                                .read(cx)
1704                                .channel_id()?;
1705
1706                            Some(call_channel == channel.id)
1707                        })
1708                        .unwrap_or(false);
1709                        if is_active {
1710                            self.open_channel_notes(channel.id, cx)
1711                        } else {
1712                            self.join_channel(channel.id, cx)
1713                        }
1714                    }
1715                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
1716                    _ => {}
1717                }
1718            }
1719        }
1720    }
1721
1722    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
1723        if self.channel_editing_state.is_some() {
1724            self.channel_name_editor.update(cx, |editor, cx| {
1725                editor.insert(" ", cx);
1726            });
1727        }
1728    }
1729
1730    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
1731        if let Some(editing_state) = &mut self.channel_editing_state {
1732            match editing_state {
1733                ChannelEditingState::Create {
1734                    location,
1735                    pending_name,
1736                    ..
1737                } => {
1738                    if pending_name.is_some() {
1739                        return false;
1740                    }
1741                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1742
1743                    *pending_name = Some(channel_name.clone());
1744
1745                    self.channel_store
1746                        .update(cx, |channel_store, cx| {
1747                            channel_store.create_channel(&channel_name, *location, cx)
1748                        })
1749                        .detach();
1750                    cx.notify();
1751                }
1752                ChannelEditingState::Rename {
1753                    location,
1754                    pending_name,
1755                } => {
1756                    if pending_name.is_some() {
1757                        return false;
1758                    }
1759                    let channel_name = self.channel_name_editor.read(cx).text(cx);
1760                    *pending_name = Some(channel_name.clone());
1761
1762                    self.channel_store
1763                        .update(cx, |channel_store, cx| {
1764                            channel_store.rename(*location, &channel_name, cx)
1765                        })
1766                        .detach();
1767                    cx.notify();
1768                }
1769            }
1770            cx.focus_self();
1771            true
1772        } else {
1773            false
1774        }
1775    }
1776
1777    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1778        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1779            self.collapsed_sections.remove(ix);
1780        } else {
1781            self.collapsed_sections.push(section);
1782        }
1783        self.update_entries(false, cx);
1784    }
1785
1786    fn collapse_selected_channel(
1787        &mut self,
1788        _: &CollapseSelectedChannel,
1789        cx: &mut ViewContext<Self>,
1790    ) {
1791        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
1792            return;
1793        };
1794
1795        if self.is_channel_collapsed(channel_id) {
1796            return;
1797        }
1798
1799        self.toggle_channel_collapsed(channel_id, cx);
1800    }
1801
1802    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
1803        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
1804            return;
1805        };
1806
1807        if !self.is_channel_collapsed(id) {
1808            return;
1809        }
1810
1811        self.toggle_channel_collapsed(id, cx)
1812    }
1813
1814    //     fn toggle_channel_collapsed_action(
1815    //         &mut self,
1816    //         action: &ToggleCollapse,
1817    //         cx: &mut ViewContext<Self>,
1818    //     ) {
1819    //         self.toggle_channel_collapsed(action.location, cx);
1820    //     }
1821
1822    fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1823        match self.collapsed_channels.binary_search(&channel_id) {
1824            Ok(ix) => {
1825                self.collapsed_channels.remove(ix);
1826            }
1827            Err(ix) => {
1828                self.collapsed_channels.insert(ix, channel_id);
1829            }
1830        };
1831        self.serialize(cx);
1832        self.update_entries(true, cx);
1833        cx.notify();
1834        cx.focus_self();
1835    }
1836
1837    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
1838        self.collapsed_channels.binary_search(&channel_id).is_ok()
1839    }
1840
1841    fn leave_call(cx: &mut ViewContext<Self>) {
1842        ActiveCall::global(cx)
1843            .update(cx, |call, cx| call.hang_up(cx))
1844            .detach_and_log_err(cx);
1845    }
1846
1847    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1848        if let Some(workspace) = self.workspace.upgrade() {
1849            workspace.update(cx, |workspace, cx| {
1850                workspace.toggle_modal(cx, |cx| {
1851                    let mut finder = ContactFinder::new(self.user_store.clone(), cx);
1852                    finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1853                    finder
1854                });
1855            });
1856        }
1857    }
1858
1859    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
1860        self.channel_editing_state = Some(ChannelEditingState::Create {
1861            location: None,
1862            pending_name: None,
1863        });
1864        self.update_entries(false, cx);
1865        self.select_channel_editor();
1866        cx.focus_view(&self.channel_name_editor);
1867        cx.notify();
1868    }
1869
1870    fn select_channel_editor(&mut self) {
1871        self.selection = self.entries.iter().position(|entry| match entry {
1872            ListEntry::ChannelEditor { .. } => true,
1873            _ => false,
1874        });
1875    }
1876
1877    fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1878        self.collapsed_channels
1879            .retain(|channel| *channel != channel_id);
1880        self.channel_editing_state = Some(ChannelEditingState::Create {
1881            location: Some(channel_id),
1882            pending_name: None,
1883        });
1884        self.update_entries(false, cx);
1885        self.select_channel_editor();
1886        cx.focus_view(&self.channel_name_editor);
1887        cx.notify();
1888    }
1889
1890    fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1891        self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
1892    }
1893
1894    fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1895        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
1896    }
1897
1898    fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
1899        if let Some(channel) = self.selected_channel() {
1900            self.remove_channel(channel.id, cx)
1901        }
1902    }
1903
1904    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
1905        if let Some(channel) = self.selected_channel() {
1906            self.rename_channel(channel.id, cx);
1907        }
1908    }
1909
1910    fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1911        let channel_store = self.channel_store.read(cx);
1912        if !channel_store.is_channel_admin(channel_id) {
1913            return;
1914        }
1915        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
1916            self.channel_editing_state = Some(ChannelEditingState::Rename {
1917                location: channel_id,
1918                pending_name: None,
1919            });
1920            self.channel_name_editor.update(cx, |editor, cx| {
1921                editor.set_text(channel.name.clone(), cx);
1922                editor.select_all(&Default::default(), cx);
1923            });
1924            cx.focus_view(&self.channel_name_editor);
1925            self.update_entries(false, cx);
1926            self.select_channel_editor();
1927        }
1928    }
1929
1930    fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1931        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
1932    }
1933
1934    fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1935        if let Some(channel) = self.selected_channel() {
1936            self.channel_clipboard = Some(ChannelMoveClipboard {
1937                channel_id: channel.id,
1938            })
1939        }
1940    }
1941
1942    fn move_channel_on_clipboard(
1943        &mut self,
1944        to_channel_id: ChannelId,
1945        cx: &mut ViewContext<CollabPanel>,
1946    ) {
1947        if let Some(clipboard) = self.channel_clipboard.take() {
1948            self.channel_store.update(cx, |channel_store, cx| {
1949                channel_store
1950                    .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
1951                    .detach_and_log_err(cx)
1952            })
1953        }
1954    }
1955
1956    fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
1957        if let Some(workspace) = self.workspace.upgrade() {
1958            ChannelView::open(channel_id, workspace, cx).detach();
1959        }
1960    }
1961
1962    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
1963        let Some(channel) = self.selected_channel() else {
1964            return;
1965        };
1966        let Some(bounds) = self
1967            .selection
1968            .and_then(|ix| self.scroll_handle.bounds_for_item(ix))
1969        else {
1970            return;
1971        };
1972
1973        self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx);
1974        cx.stop_propagation();
1975    }
1976
1977    fn selected_channel(&self) -> Option<&Arc<Channel>> {
1978        self.selection
1979            .and_then(|ix| self.entries.get(ix))
1980            .and_then(|entry| match entry {
1981                ListEntry::Channel { channel, .. } => Some(channel),
1982                _ => None,
1983            })
1984    }
1985
1986    fn show_channel_modal(
1987        &mut self,
1988        channel_id: ChannelId,
1989        mode: channel_modal::Mode,
1990        cx: &mut ViewContext<Self>,
1991    ) {
1992        let workspace = self.workspace.clone();
1993        let user_store = self.user_store.clone();
1994        let channel_store = self.channel_store.clone();
1995        let members = self.channel_store.update(cx, |channel_store, cx| {
1996            channel_store.get_channel_member_details(channel_id, cx)
1997        });
1998
1999        cx.spawn(|_, mut cx| async move {
2000            let members = members.await?;
2001            workspace.update(&mut cx, |workspace, cx| {
2002                workspace.toggle_modal(cx, |cx| {
2003                    ChannelModal::new(
2004                        user_store.clone(),
2005                        channel_store.clone(),
2006                        channel_id,
2007                        mode,
2008                        members,
2009                        cx,
2010                    )
2011                });
2012            })
2013        })
2014        .detach();
2015    }
2016
2017    //     fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
2018    //         self.remove_channel(action.channel_id, cx)
2019    //     }
2020
2021    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2022        let channel_store = self.channel_store.clone();
2023        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2024            let prompt_message = format!(
2025                "Are you sure you want to remove the channel \"{}\"?",
2026                channel.name
2027            );
2028            let mut answer =
2029                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2030            let window = cx.window();
2031            cx.spawn(|this, mut cx| async move {
2032                if answer.await? == 0 {
2033                    channel_store
2034                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
2035                        .await
2036                        .notify_async_err(&mut cx);
2037                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
2038                }
2039                anyhow::Ok(())
2040            })
2041            .detach();
2042        }
2043    }
2044
2045    //     // Should move to the filter editor if clicking on it
2046    //     // Should move selection to the channel editor if activating it
2047
2048    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2049        let user_store = self.user_store.clone();
2050        let prompt_message = format!(
2051            "Are you sure you want to remove \"{}\" from your contacts?",
2052            github_login
2053        );
2054        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2055        let window = cx.window();
2056        cx.spawn(|_, mut cx| async move {
2057            if answer.await? == 0 {
2058                user_store
2059                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
2060                    .await
2061                    .notify_async_err(&mut cx);
2062            }
2063            anyhow::Ok(())
2064        })
2065        .detach_and_log_err(cx);
2066    }
2067
2068    fn respond_to_contact_request(
2069        &mut self,
2070        user_id: u64,
2071        accept: bool,
2072        cx: &mut ViewContext<Self>,
2073    ) {
2074        self.user_store
2075            .update(cx, |store, cx| {
2076                store.respond_to_contact_request(user_id, accept, cx)
2077            })
2078            .detach_and_log_err(cx);
2079    }
2080
2081    //     fn respond_to_channel_invite(
2082    //         &mut self,
2083    //         channel_id: u64,
2084    //         accept: bool,
2085    //         cx: &mut ViewContext<Self>,
2086    //     ) {
2087    //         self.channel_store
2088    //             .update(cx, |store, cx| {
2089    //                 store.respond_to_channel_invite(channel_id, accept, cx)
2090    //             })
2091    //             .detach();
2092    //     }
2093
2094    fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext<Self>) {
2095        ActiveCall::global(cx)
2096            .update(cx, |call, cx| {
2097                call.invite(recipient_user_id, Some(self.project.clone()), cx)
2098            })
2099            .detach_and_log_err(cx);
2100    }
2101
2102    fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
2103        let Some(workspace) = self.workspace.upgrade() else {
2104            return;
2105        };
2106        let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
2107            return;
2108        };
2109        workspace::join_channel(
2110            channel_id,
2111            workspace.read(cx).app_state().clone(),
2112            Some(handle),
2113            cx,
2114        )
2115        .detach_and_log_err(cx)
2116    }
2117
2118    fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2119        let Some(workspace) = self.workspace.upgrade() else {
2120            return;
2121        };
2122        cx.window_context().defer(move |cx| {
2123            workspace.update(cx, |workspace, cx| {
2124                if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
2125                    panel.update(cx, |panel, cx| {
2126                        panel
2127                            .select_channel(channel_id, None, cx)
2128                            .detach_and_log_err(cx);
2129                    });
2130                }
2131            });
2132        });
2133    }
2134
2135    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2136        let channel_store = self.channel_store.read(cx);
2137        let Some(channel) = channel_store.channel_for_id(channel_id) else {
2138            return;
2139        };
2140        let item = ClipboardItem::new(channel.link());
2141        cx.write_to_clipboard(item)
2142    }
2143
2144    fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
2145        v_stack().border_1().border_color(gpui::red()).child(
2146            Button::new("sign_in", "Sign in to collaborate").on_click(cx.listener(
2147                |this, _, cx| {
2148                    let client = this.client.clone();
2149                    cx.spawn(|_, mut cx| async move {
2150                        client
2151                            .authenticate_and_connect(true, &cx)
2152                            .await
2153                            .notify_async_err(&mut cx);
2154                    })
2155                    .detach()
2156                },
2157            )),
2158        )
2159    }
2160
2161    fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
2162        v_stack()
2163            .size_full()
2164            .child(
2165                v_stack()
2166                    .size_full()
2167                    .id("scroll")
2168                    .overflow_y_scroll()
2169                    .track_scroll(&self.scroll_handle)
2170                    .children(self.entries.iter().enumerate().map(|(ix, entry)| {
2171                        let is_selected = self.selection == Some(ix);
2172                        match entry {
2173                            ListEntry::Header(section) => {
2174                                let is_collapsed = self.collapsed_sections.contains(section);
2175                                self.render_header(*section, is_selected, is_collapsed, cx)
2176                                    .into_any_element()
2177                            }
2178                            ListEntry::Contact { contact, calling } => self
2179                                .render_contact(contact, *calling, is_selected, cx)
2180                                .into_any_element(),
2181                            ListEntry::ContactPlaceholder => self
2182                                .render_contact_placeholder(is_selected, cx)
2183                                .into_any_element(),
2184                            ListEntry::IncomingRequest(user) => self
2185                                .render_contact_request(user, true, is_selected, cx)
2186                                .into_any_element(),
2187                            ListEntry::OutgoingRequest(user) => self
2188                                .render_contact_request(user, false, is_selected, cx)
2189                                .into_any_element(),
2190                            ListEntry::Channel {
2191                                channel,
2192                                depth,
2193                                has_children,
2194                            } => self
2195                                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
2196                                .into_any_element(),
2197                            ListEntry::ChannelEditor { depth } => {
2198                                self.render_channel_editor(*depth, cx).into_any_element()
2199                            }
2200                            ListEntry::CallParticipant {
2201                                user,
2202                                peer_id,
2203                                is_pending,
2204                            } => self
2205                                .render_call_participant(user, *peer_id, *is_pending, cx)
2206                                .into_any_element(),
2207                            ListEntry::ParticipantProject {
2208                                project_id,
2209                                worktree_root_names,
2210                                host_user_id,
2211                                is_last,
2212                            } => self
2213                                .render_participant_project(
2214                                    *project_id,
2215                                    &worktree_root_names,
2216                                    *host_user_id,
2217                                    *is_last,
2218                                    cx,
2219                                )
2220                                .into_any_element(),
2221                            ListEntry::ParticipantScreen { peer_id, is_last } => self
2222                                .render_participant_screen(*peer_id, *is_last, cx)
2223                                .into_any_element(),
2224                            ListEntry::ChannelNotes { channel_id } => self
2225                                .render_channel_notes(*channel_id, cx)
2226                                .into_any_element(),
2227                            ListEntry::ChannelChat { channel_id } => {
2228                                self.render_channel_chat(*channel_id, cx).into_any_element()
2229                            }
2230                        }
2231                    })),
2232            )
2233            .child(
2234                div().p_2().child(
2235                    div()
2236                        .border_primary(cx)
2237                        .border_t()
2238                        .child(self.filter_editor.clone()),
2239                ),
2240            )
2241    }
2242
2243    fn render_header(
2244        &self,
2245        section: Section,
2246        is_selected: bool,
2247        is_collapsed: bool,
2248        cx: &ViewContext<Self>,
2249    ) -> impl IntoElement {
2250        let mut channel_link = None;
2251        let mut channel_tooltip_text = None;
2252        let mut channel_icon = None;
2253        // let mut is_dragged_over = false;
2254
2255        let text = match section {
2256            Section::ActiveCall => {
2257                let channel_name = maybe!({
2258                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2259
2260                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2261
2262                    channel_link = Some(channel.link());
2263                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2264                        proto::ChannelVisibility::Public => {
2265                            (Some("icons/public.svg"), Some("Copy public channel link."))
2266                        }
2267                        proto::ChannelVisibility::Members => {
2268                            (Some("icons/hash.svg"), Some("Copy private channel link."))
2269                        }
2270                    };
2271
2272                    Some(channel.name.as_ref())
2273                });
2274
2275                if let Some(name) = channel_name {
2276                    SharedString::from(format!("{}", name))
2277                } else {
2278                    SharedString::from("Current Call")
2279                }
2280            }
2281            Section::ContactRequests => SharedString::from("Requests"),
2282            Section::Contacts => SharedString::from("Contacts"),
2283            Section::Channels => SharedString::from("Channels"),
2284            Section::ChannelInvites => SharedString::from("Invites"),
2285            Section::Online => SharedString::from("Online"),
2286            Section::Offline => SharedString::from("Offline"),
2287        };
2288
2289        let button = match section {
2290            Section::ActiveCall => channel_link.map(|channel_link| {
2291                let channel_link_copy = channel_link.clone();
2292                IconButton::new("channel-link", Icon::Copy)
2293                    .icon_size(IconSize::Small)
2294                    .size(ButtonSize::None)
2295                    .visible_on_hover("section-header")
2296                    .on_click(move |_, cx| {
2297                        let item = ClipboardItem::new(channel_link_copy.clone());
2298                        cx.write_to_clipboard(item)
2299                    })
2300                    .tooltip(|cx| Tooltip::text("Copy channel link", cx))
2301                    .into_any_element()
2302            }),
2303            Section::Contacts => Some(
2304                div()
2305                    .border_1()
2306                    .border_color(gpui::red())
2307                    .child(
2308                        IconButton::new("add-contact", Icon::Plus)
2309                            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2310                            .tooltip(|cx| Tooltip::text("Search for new contact", cx)),
2311                    )
2312                    .into_any_element(),
2313            ),
2314            Section::Channels => Some(
2315                IconButton::new("add-channel", Icon::Plus)
2316                    .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
2317                    .tooltip(|cx| Tooltip::text("Create a channel", cx))
2318                    .into_any_element(),
2319            ),
2320            _ => None,
2321        };
2322
2323        let can_collapse = match section {
2324            Section::ActiveCall | Section::Channels | Section::Contacts => false,
2325            Section::ChannelInvites
2326            | Section::ContactRequests
2327            | Section::Online
2328            | Section::Offline => true,
2329        };
2330
2331        let mut row = h_stack()
2332            .w_full()
2333            .group("section-header")
2334            .child(
2335                ListHeader::new(text)
2336                    .toggle(if can_collapse {
2337                        Some(!is_collapsed)
2338                    } else {
2339                        None
2340                    })
2341                    .inset(true)
2342                    .end_slot::<AnyElement>(button)
2343                    .selected(is_selected),
2344            )
2345            .when(section == Section::Channels, |el| {
2346                el.drag_over::<DraggedChannelView>(|style| {
2347                    style.bg(cx.theme().colors().ghost_element_hover)
2348                })
2349                .on_drop(cx.listener(
2350                    move |this, view: &View<DraggedChannelView>, cx| {
2351                        this.channel_store
2352                            .update(cx, |channel_store, cx| {
2353                                channel_store.move_channel(view.read(cx).channel.id, None, cx)
2354                            })
2355                            .detach_and_log_err(cx)
2356                    },
2357                ))
2358            });
2359
2360        if section == Section::Offline {
2361            row = div().border_1().border_color(gpui::red()).child(row);
2362        }
2363
2364        row
2365    }
2366
2367    fn render_contact(
2368        &self,
2369        contact: &Contact,
2370        calling: bool,
2371        is_selected: bool,
2372        cx: &mut ViewContext<Self>,
2373    ) -> impl IntoElement {
2374        let online = contact.online;
2375        let busy = contact.busy || calling;
2376        let user_id = contact.user.id;
2377        let github_login = SharedString::from(contact.user.github_login.clone());
2378        let mut item =
2379            ListItem::new(github_login.clone())
2380                .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2381                .child(
2382                    h_stack()
2383                        .w_full()
2384                        .justify_between()
2385                        .child(Label::new(github_login.clone()))
2386                        .when(calling, |el| {
2387                            el.child(Label::new("Calling").color(Color::Muted))
2388                        })
2389                        .when(!calling, |el| {
2390                            el.child(
2391                                IconButton::new("remove_contact", Icon::Close)
2392                                    .icon_color(Color::Muted)
2393                                    .visible_on_hover("")
2394                                    .tooltip(|cx| Tooltip::text("Remove Contact", cx))
2395                                    .on_click(cx.listener({
2396                                        let github_login = github_login.clone();
2397                                        move |this, _, cx| {
2398                                            this.remove_contact(user_id, &github_login, cx);
2399                                        }
2400                                    })),
2401                            )
2402                        }),
2403                )
2404                .start_slot(
2405                    // todo!() handle contacts with no avatar
2406                    Avatar::new(contact.user.avatar_uri.clone())
2407                        .availability_indicator(if online { Some(!busy) } else { None }),
2408                )
2409                .when(online && !busy, |el| {
2410                    el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2411                });
2412
2413        div()
2414            .id(github_login.clone())
2415            .group("")
2416            .child(item)
2417            .tooltip(move |cx| {
2418                let text = if !online {
2419                    format!(" {} is offline", &github_login)
2420                } else if busy {
2421                    format!(" {} is on a call", &github_login)
2422                } else {
2423                    let room = ActiveCall::global(cx).read(cx).room();
2424                    if room.is_some() {
2425                        format!("Invite {} to join call", &github_login)
2426                    } else {
2427                        format!("Call {}", &github_login)
2428                    }
2429                };
2430                Tooltip::text(text, cx)
2431            })
2432    }
2433
2434    fn render_contact_request(
2435        &self,
2436        user: &Arc<User>,
2437        is_incoming: bool,
2438        is_selected: bool,
2439        cx: &mut ViewContext<Self>,
2440    ) -> impl IntoElement {
2441        let github_login = SharedString::from(user.github_login.clone());
2442        let user_id = user.id;
2443        let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2444        let color = if is_contact_request_pending {
2445            Color::Muted
2446        } else {
2447            Color::Default
2448        };
2449
2450        let controls = if is_incoming {
2451            vec![
2452                IconButton::new("remove_contact", Icon::Close)
2453                    .on_click(cx.listener(move |this, _, cx| {
2454                        this.respond_to_contact_request(user_id, false, cx);
2455                    }))
2456                    .icon_color(color)
2457                    .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2458                IconButton::new("remove_contact", Icon::Check)
2459                    .on_click(cx.listener(move |this, _, cx| {
2460                        this.respond_to_contact_request(user_id, true, cx);
2461                    }))
2462                    .icon_color(color)
2463                    .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2464            ]
2465        } else {
2466            let github_login = github_login.clone();
2467            vec![IconButton::new("remove_contact", Icon::Close)
2468                .on_click(cx.listener(move |this, _, cx| {
2469                    this.remove_contact(user_id, &github_login, cx);
2470                }))
2471                .icon_color(color)
2472                .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2473        };
2474
2475        ListItem::new(github_login.clone())
2476            .child(
2477                h_stack()
2478                    .w_full()
2479                    .justify_between()
2480                    .child(Label::new(github_login.clone()))
2481                    .child(h_stack().children(controls)),
2482            )
2483            .start_slot(Avatar::new(user.avatar_uri.clone()))
2484    }
2485
2486    fn render_contact_placeholder(
2487        &self,
2488        is_selected: bool,
2489        cx: &mut ViewContext<Self>,
2490    ) -> impl IntoElement {
2491        ListItem::new("contact-placeholder")
2492            .child(IconElement::new(Icon::Plus))
2493            .child(Label::new("Add a Contact"))
2494            .selected(is_selected)
2495            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2496    }
2497
2498    fn render_channel(
2499        &self,
2500        channel: &Channel,
2501        depth: usize,
2502        has_children: bool,
2503        is_selected: bool,
2504        ix: usize,
2505        cx: &mut ViewContext<Self>,
2506    ) -> impl IntoElement {
2507        let channel_id = channel.id;
2508
2509        let is_active = maybe!({
2510            let call_channel = ActiveCall::global(cx)
2511                .read(cx)
2512                .room()?
2513                .read(cx)
2514                .channel_id()?;
2515            Some(call_channel == channel_id)
2516        })
2517        .unwrap_or(false);
2518        let is_public = self
2519            .channel_store
2520            .read(cx)
2521            .channel_for_id(channel_id)
2522            .map(|channel| channel.visibility)
2523            == Some(proto::ChannelVisibility::Public);
2524        let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2525        let disclosed =
2526            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2527
2528        let has_messages_notification = channel.unseen_message_id.is_some();
2529        let has_notes_notification = channel.unseen_note_version.is_some();
2530
2531        const FACEPILE_LIMIT: usize = 3;
2532        let participants = self.channel_store.read(cx).channel_participants(channel_id);
2533
2534        let face_pile = if !participants.is_empty() {
2535            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2536            let user = &participants[0];
2537
2538            let result = FacePile {
2539                faces: participants
2540                    .iter()
2541                    .filter_map(|user| {
2542                        Some(Avatar::new(user.avatar_uri.clone()).into_any_element())
2543                    })
2544                    .take(FACEPILE_LIMIT)
2545                    .chain(if extra_count > 0 {
2546                        // todo!() @nate - this label looks wrong.
2547                        Some(Label::new(format!("+{}", extra_count)).into_any_element())
2548                    } else {
2549                        None
2550                    })
2551                    .collect::<SmallVec<_>>(),
2552            };
2553
2554            Some(result)
2555        } else {
2556            None
2557        };
2558
2559        let width = self.width.unwrap_or(px(240.));
2560
2561        div()
2562            .id(channel_id as usize)
2563            .group("")
2564            .flex()
2565            .w_full()
2566            .on_drag(channel.clone(), move |channel, cx| {
2567                cx.build_view(|cx| DraggedChannelView {
2568                    channel: channel.clone(),
2569                    width,
2570                })
2571            })
2572            .drag_over::<DraggedChannelView>(|style| {
2573                style.bg(cx.theme().colors().ghost_element_hover)
2574            })
2575            .on_drop(
2576                cx.listener(move |this, view: &View<DraggedChannelView>, cx| {
2577                    this.channel_store
2578                        .update(cx, |channel_store, cx| {
2579                            channel_store.move_channel(
2580                                view.read(cx).channel.id,
2581                                Some(channel_id),
2582                                cx,
2583                            )
2584                        })
2585                        .detach_and_log_err(cx)
2586                }),
2587            )
2588            .child(
2589                ListItem::new(channel_id as usize)
2590                    // Offset the indent depth by one to give us room to show the disclosure.
2591                    .indent_level(depth + 1)
2592                    .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to  step over the disclosure toggle
2593                    .selected(is_selected || is_active)
2594                    .toggle(disclosed)
2595                    .on_toggle(
2596                        cx.listener(move |this, _, cx| {
2597                            this.toggle_channel_collapsed(channel_id, cx)
2598                        }),
2599                    )
2600                    .on_click(cx.listener(move |this, _, cx| {
2601                        if this.drag_target_channel == ChannelDragTarget::None {
2602                            if is_active {
2603                                this.open_channel_notes(channel_id, cx)
2604                            } else {
2605                                this.join_channel(channel_id, cx)
2606                            }
2607                        }
2608                    }))
2609                    .on_secondary_mouse_down(cx.listener(
2610                        move |this, event: &MouseDownEvent, cx| {
2611                            this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2612                        },
2613                    ))
2614                    .start_slot(
2615                        IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
2616                            .size(IconSize::Small)
2617                            .color(Color::Muted),
2618                    )
2619                    .child(
2620                        h_stack()
2621                            .id(channel_id as usize)
2622                            .child(Label::new(channel.name.clone()))
2623                            .children(face_pile.map(|face_pile| face_pile.render(cx))),
2624                    )
2625                    .end_slot(
2626                        h_stack()
2627                            .child(
2628                                IconButton::new("channel_chat", Icon::MessageBubbles)
2629                                    .icon_color(if has_messages_notification {
2630                                        Color::Default
2631                                    } else {
2632                                        Color::Muted
2633                                    })
2634                                    .when(!has_messages_notification, |this| {
2635                                        this.visible_on_hover("")
2636                                    })
2637                                    .on_click(cx.listener(move |this, _, cx| {
2638                                        this.join_channel_chat(channel_id, cx)
2639                                    }))
2640                                    .tooltip(|cx| Tooltip::text("Open channel chat", cx)),
2641                            )
2642                            .child(
2643                                IconButton::new("channel_notes", Icon::File)
2644                                    .icon_color(if has_notes_notification {
2645                                        Color::Default
2646                                    } else {
2647                                        Color::Muted
2648                                    })
2649                                    .when(!has_notes_notification, |this| this.visible_on_hover(""))
2650                                    .on_click(cx.listener(move |this, _, cx| {
2651                                        this.open_channel_notes(channel_id, cx)
2652                                    }))
2653                                    .tooltip(|cx| Tooltip::text("Open channel notes", cx)),
2654                            ),
2655                    ),
2656            )
2657            .tooltip(|cx| Tooltip::text("Join channel", cx))
2658
2659        // let channel_id = channel.id;
2660        // let collab_theme = &theme.collab_panel;
2661        // let is_public = self
2662        //     .channel_store
2663        //     .read(cx)
2664        //     .channel_for_id(channel_id)
2665        //     .map(|channel| channel.visibility)
2666        //     == Some(proto::ChannelVisibility::Public);
2667        // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2668        // let disclosed =
2669        //     has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2670
2671        // enum ChannelCall {}
2672        // enum ChannelNote {}
2673        // enum NotesTooltip {}
2674        // enum ChatTooltip {}
2675        // enum ChannelTooltip {}
2676
2677        // let mut is_dragged_over = false;
2678        // if cx
2679        //     .global::<DragAndDrop<Workspace>>()
2680        //     .currently_dragged::<Channel>(cx.window())
2681        //     .is_some()
2682        //     && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
2683        // {
2684        //     is_dragged_over = true;
2685        // }
2686
2687        // let has_messages_notification = channel.unseen_message_id.is_some();
2688
2689        // MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
2690        //     let row_hovered = state.hovered();
2691
2692        //     let mut select_state = |interactive: &Interactive<ContainerStyle>| {
2693        //         if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
2694        //             interactive.clicked.as_ref().unwrap().clone()
2695        //         } else if state.hovered() || other_selected {
2696        //             interactive
2697        //                 .hovered
2698        //                 .as_ref()
2699        //                 .unwrap_or(&interactive.default)
2700        //                 .clone()
2701        //         } else {
2702        //             interactive.default.clone()
2703        //         }
2704        //     };
2705
2706        //     Flex::<Self>::row()
2707        //         .with_child(
2708        //             Svg::new(if is_public {
2709        //                 "icons/public.svg"
2710        //             } else {
2711        //                 "icons/hash.svg"
2712        //             })
2713        //             .with_color(collab_theme.channel_hash.color)
2714        //             .constrained()
2715        //             .with_width(collab_theme.channel_hash.width)
2716        //             .aligned()
2717        //             .left(),
2718        //         )
2719        //         .with_child({
2720        //             let style = collab_theme.channel_name.inactive_state();
2721        //             Flex::row()
2722        //                 .with_child(
2723        //                     Label::new(channel.name.clone(), style.text.clone())
2724        //                         .contained()
2725        //                         .with_style(style.container)
2726        //                         .aligned()
2727        //                         .left()
2728        //                         .with_tooltip::<ChannelTooltip>(
2729        //                             ix,
2730        //                             "Join channel",
2731        //                             None,
2732        //                             theme.tooltip.clone(),
2733        //                             cx,
2734        //                         ),
2735        //                 )
2736        //                 .with_children({
2737        //                     let participants =
2738        //                         self.channel_store.read(cx).channel_participants(channel_id);
2739
2740        //                     if !participants.is_empty() {
2741        //                         let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2742
2743        //                         let result = FacePile::new(collab_theme.face_overlap)
2744        //                             .with_children(
2745        //                                 participants
2746        //                                     .iter()
2747        //                                     .filter_map(|user| {
2748        //                                         Some(
2749        //                                             Image::from_data(user.avatar.clone()?)
2750        //                                                 .with_style(collab_theme.channel_avatar),
2751        //                                         )
2752        //                                     })
2753        //                                     .take(FACEPILE_LIMIT),
2754        //                             )
2755        //                             .with_children((extra_count > 0).then(|| {
2756        //                                 Label::new(
2757        //                                     format!("+{}", extra_count),
2758        //                                     collab_theme.extra_participant_label.text.clone(),
2759        //                                 )
2760        //                                 .contained()
2761        //                                 .with_style(collab_theme.extra_participant_label.container)
2762        //                             }));
2763
2764        //                         Some(result)
2765        //                     } else {
2766        //                         None
2767        //                     }
2768        //                 })
2769        //                 .with_spacing(8.)
2770        //                 .align_children_center()
2771        //                 .flex(1., true)
2772        //         })
2773        //         .with_child(
2774        //             MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
2775        //                 let container_style = collab_theme
2776        //                     .disclosure
2777        //                     .button
2778        //                     .style_for(mouse_state)
2779        //                     .container;
2780
2781        //                 if channel.unseen_message_id.is_some() {
2782        //                     Svg::new("icons/conversations.svg")
2783        //                         .with_color(collab_theme.channel_note_active_color)
2784        //                         .constrained()
2785        //                         .with_width(collab_theme.channel_hash.width)
2786        //                         .contained()
2787        //                         .with_style(container_style)
2788        //                         .with_uniform_padding(4.)
2789        //                         .into_any()
2790        //                 } else if row_hovered {
2791        //                     Svg::new("icons/conversations.svg")
2792        //                         .with_color(collab_theme.channel_hash.color)
2793        //                         .constrained()
2794        //                         .with_width(collab_theme.channel_hash.width)
2795        //                         .contained()
2796        //                         .with_style(container_style)
2797        //                         .with_uniform_padding(4.)
2798        //                         .into_any()
2799        //                 } else {
2800        //                     Empty::new().into_any()
2801        //                 }
2802        //             })
2803        //             .on_click(MouseButton::Left, move |_, this, cx| {
2804        //                 this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
2805        //             })
2806        //             .with_tooltip::<ChatTooltip>(
2807        //                 ix,
2808        //                 "Open channel chat",
2809        //                 None,
2810        //                 theme.tooltip.clone(),
2811        //                 cx,
2812        //             )
2813        //             .contained()
2814        //             .with_margin_right(4.),
2815        //         )
2816        //         .with_child(
2817        //             MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
2818        //                 let container_style = collab_theme
2819        //                     .disclosure
2820        //                     .button
2821        //                     .style_for(mouse_state)
2822        //                     .container;
2823        //                 if row_hovered || channel.unseen_note_version.is_some() {
2824        //                     Svg::new("icons/file.svg")
2825        //                         .with_color(if channel.unseen_note_version.is_some() {
2826        //                             collab_theme.channel_note_active_color
2827        //                         } else {
2828        //                             collab_theme.channel_hash.color
2829        //                         })
2830        //                         .constrained()
2831        //                         .with_width(collab_theme.channel_hash.width)
2832        //                         .contained()
2833        //                         .with_style(container_style)
2834        //                         .with_uniform_padding(4.)
2835        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2836        //                         .with_tooltip::<NotesTooltip>(
2837        //                             ix as usize,
2838        //                             "Open channel notes",
2839        //                             None,
2840        //                             theme.tooltip.clone(),
2841        //                             cx,
2842        //                         )
2843        //                         .into_any()
2844        //                 } else if has_messages_notification {
2845        //                     Empty::new()
2846        //                         .constrained()
2847        //                         .with_width(collab_theme.channel_hash.width)
2848        //                         .contained()
2849        //                         .with_uniform_padding(4.)
2850        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2851        //                         .into_any()
2852        //                 } else {
2853        //                     Empty::new().into_any()
2854        //                 }
2855        //             })
2856        //             .on_click(MouseButton::Left, move |_, this, cx| {
2857        //                 this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2858        //             }),
2859        //         )
2860        //         .align_children_center()
2861        //         .styleable_component()
2862        //         .disclosable(
2863        //             disclosed,
2864        //             Box::new(ToggleCollapse {
2865        //                 location: channel.id.clone(),
2866        //             }),
2867        //         )
2868        //         .with_id(ix)
2869        //         .with_style(collab_theme.disclosure.clone())
2870        //         .element()
2871        //         .constrained()
2872        //         .with_height(collab_theme.row_height)
2873        //         .contained()
2874        //         .with_style(select_state(
2875        //             collab_theme
2876        //                 .channel_row
2877        //                 .in_state(is_selected || is_active || is_dragged_over),
2878        //         ))
2879        //         .with_padding_left(
2880        //             collab_theme.channel_row.default_style().padding.left
2881        //                 + collab_theme.channel_indent * depth as f32,
2882        //         )
2883        // })
2884        // .on_click(MouseButton::Left, move |_, this, cx| {
2885        //     if this.
2886        // drag_target_channel == ChannelDragTarget::None {
2887        //         if is_active {
2888        //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
2889        //         } else {
2890        //             this.join_channel(channel_id, cx)
2891        //         }
2892        //     }
2893        // })
2894        // .on_click(MouseButton::Right, {
2895        //     let channel = channel.clone();
2896        //     move |e, this, cx| {
2897        //         this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
2898        //     }
2899        // })
2900        // .on_up(MouseButton::Left, move |_, this, cx| {
2901        //     if let Some((_, dragged_channel)) = cx
2902        //         .global::<DragAndDrop<Workspace>>()
2903        //         .currently_dragged::<Channel>(cx.window())
2904        //     {
2905        //         this.channel_store
2906        //             .update(cx, |channel_store, cx| {
2907        //                 channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2908        //             })
2909        //             .detach_and_log_err(cx)
2910        //     }
2911        // })
2912        // .on_move({
2913        //     let channel = channel.clone();
2914        //     move |_, this, cx| {
2915        //         if let Some((_, dragged_channel)) = cx
2916        //             .global::<DragAndDrop<Workspace>>()
2917        //             .currently_dragged::<Channel>(cx.window())
2918        //         {
2919        //             if channel.id != dragged_channel.id {
2920        //                 this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
2921        //             }
2922        //             cx.notify()
2923        //         }
2924        //     }
2925        // })
2926        // .as_draggable::<_, Channel>(
2927        //     channel.clone(),
2928        //     move |_, channel, cx: &mut ViewContext<Workspace>| {
2929        //         let theme = &theme::current(cx).collab_panel;
2930
2931        //         Flex::<Workspace>::row()
2932        //             .with_child(
2933        //                 Svg::new("icons/hash.svg")
2934        //                     .with_color(theme.channel_hash.color)
2935        //                     .constrained()
2936        //                     .with_width(theme.channel_hash.width)
2937        //                     .aligned()
2938        //                     .left(),
2939        //             )
2940        //             .with_child(
2941        //                 Label::new(channel.name.clone(), theme.channel_name.text.clone())
2942        //                     .contained()
2943        //                     .with_style(theme.channel_name.container)
2944        //                     .aligned()
2945        //                     .left(),
2946        //             )
2947        //             .align_children_center()
2948        //             .contained()
2949        //             .with_background_color(
2950        //                 theme
2951        //                     .container
2952        //                     .background_color
2953        //                     .unwrap_or(gpui::color::Color::transparent_black()),
2954        //             )
2955        //             .contained()
2956        //             .with_padding_left(
2957        //                 theme.channel_row.default_style().padding.left
2958        //                     + theme.channel_indent * depth as f32,
2959        //             )
2960        //             .into_any()
2961        //     },
2962        // )
2963        // .with_cursor_style(CursorStyle::PointingHand)
2964        // .into_any()
2965    }
2966
2967    fn render_channel_editor(&self, depth: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
2968        let item = ListItem::new("channel-editor")
2969            .inset(false)
2970            .indent_level(depth)
2971            .start_slot(
2972                IconElement::new(Icon::Hash)
2973                    .size(IconSize::Small)
2974                    .color(Color::Muted),
2975            );
2976
2977        if let Some(pending_name) = self
2978            .channel_editing_state
2979            .as_ref()
2980            .and_then(|state| state.pending_name())
2981        {
2982            item.child(Label::new(pending_name))
2983        } else {
2984            item.child(
2985                div()
2986                    .w_full()
2987                    .py_1() // todo!() @nate this is a px off at the default font size.
2988                    .child(self.channel_name_editor.clone()),
2989            )
2990        }
2991    }
2992}
2993
2994fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
2995    let rem_size = cx.rem_size();
2996    let line_height = cx.text_style().line_height_in_pixels(rem_size);
2997    let width = rem_size * 1.5;
2998    let thickness = px(2.);
2999    let color = cx.theme().colors().text;
3000
3001    canvas(move |bounds, cx| {
3002        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
3003        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
3004        let right = bounds.right();
3005        let top = bounds.top();
3006
3007        cx.paint_quad(fill(
3008            Bounds::from_corners(
3009                point(start_x, top),
3010                point(
3011                    start_x + thickness,
3012                    if is_last { start_y } else { bounds.bottom() },
3013                ),
3014            ),
3015            color,
3016        ));
3017        cx.paint_quad(fill(
3018            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
3019            color,
3020        ));
3021    })
3022    .w(width)
3023    .h(line_height)
3024}
3025
3026impl Render for CollabPanel {
3027    type Element = Focusable<Div>;
3028
3029    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3030        v_stack()
3031            .key_context("CollabPanel")
3032            .on_action(cx.listener(CollabPanel::cancel))
3033            .on_action(cx.listener(CollabPanel::select_next))
3034            .on_action(cx.listener(CollabPanel::select_prev))
3035            .on_action(cx.listener(CollabPanel::confirm))
3036            .on_action(cx.listener(CollabPanel::insert_space))
3037            //     .on_action(cx.listener(CollabPanel::remove))
3038            .on_action(cx.listener(CollabPanel::remove_selected_channel))
3039            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
3040            //     .on_action(cx.listener(CollabPanel::new_subchannel))
3041            //     .on_action(cx.listener(CollabPanel::invite_members))
3042            //     .on_action(cx.listener(CollabPanel::manage_members))
3043            .on_action(cx.listener(CollabPanel::rename_selected_channel))
3044            //     .on_action(cx.listener(CollabPanel::rename_channel))
3045            //     .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action))
3046            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
3047            .on_action(cx.listener(CollabPanel::expand_selected_channel))
3048            //     .on_action(cx.listener(CollabPanel::open_channel_notes))
3049            //     .on_action(cx.listener(CollabPanel::join_channel_chat))
3050            //     .on_action(cx.listener(CollabPanel::copy_channel_link))
3051            .track_focus(&self.focus_handle)
3052            .size_full()
3053            .child(if self.user_store.read(cx).current_user().is_none() {
3054                self.render_signed_out(cx)
3055            } else {
3056                self.render_signed_in(cx)
3057            })
3058            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3059                overlay()
3060                    .position(*position)
3061                    .anchor(gpui::AnchorCorner::TopLeft)
3062                    .child(menu.clone())
3063            }))
3064    }
3065}
3066
3067// impl View for CollabPanel {
3068//     fn ui_name() -> &'static str {
3069//         "CollabPanel"
3070//     }
3071
3072//     fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
3073//         if !self.has_focus {
3074//             self.has_focus = true;
3075//             if !self.context_menu.is_focused(cx) {
3076//                 if let Some(editing_state) = &self.channel_editing_state {
3077//                     if editing_state.pending_name().is_none() {
3078//                         cx.focus(&self.channel_name_editor);
3079//                     } else {
3080//                         cx.focus(&self.filter_editor);
3081//                     }
3082//                 } else {
3083//                     cx.focus(&self.filter_editor);
3084//                 }
3085//             }
3086//             cx.emit(Event::Focus);
3087//         }
3088//     }
3089
3090//     fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
3091//         self.has_focus = false;
3092//     }
3093
3094//     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
3095//         let theme = &theme::current(cx).collab_panel;
3096
3097//         if self.user_store.read(cx).current_user().is_none() {
3098//             enum LogInButton {}
3099
3100//             return Flex::column()
3101//                 .with_child(
3102//                     MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
3103//                         let button = theme.log_in_button.style_for(state);
3104//                         Label::new("Sign in to collaborate", button.text.clone())
3105//                             .aligned()
3106//                             .left()
3107//                             .contained()
3108//                             .with_style(button.container)
3109//                     })
3110//                     .on_click(MouseButton::Left, |_, this, cx| {
3111//                         let client = this.client.clone();
3112//                         cx.spawn(|_, cx| async move {
3113//                             client.authenticate_and_connect(true, &cx).await.log_err();
3114//                         })
3115//                         .detach();
3116//                     })
3117//                     .with_cursor_style(CursorStyle::PointingHand),
3118//                 )
3119//                 .contained()
3120//                 .with_style(theme.container)
3121//                 .into_any();
3122//         }
3123
3124//         enum PanelFocus {}
3125//         MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
3126//             Stack::new()
3127//                 .with_child(
3128//                     Flex::column()
3129//                         .with_child(
3130//                             Flex::row().with_child(
3131//                                 ChildView::new(&self.filter_editor, cx)
3132//                                     .contained()
3133//                                     .with_style(theme.user_query_editor.container)
3134//                                     .flex(1.0, true),
3135//                             ),
3136//                         )
3137//                         .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3138//                         .contained()
3139//                         .with_style(theme.container)
3140//                         .into_any(),
3141//                 )
3142//                 .with_children(
3143//                     (!self.context_menu_on_selected)
3144//                         .then(|| ChildView::new(&self.context_menu, cx)),
3145//                 )
3146//                 .into_any()
3147//         })
3148//         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3149//         .into_any_named("collab panel")
3150//     }
3151
3152//     fn update_keymap_context(
3153//         &self,
3154//         keymap: &mut gpui::keymap_matcher::KeymapContext,
3155//         _: &AppContext,
3156//     ) {
3157//         Self::reset_to_default_keymap_context(keymap);
3158//         if self.channel_editing_state.is_some() {
3159//             keymap.add_identifier("editing");
3160//         } else {
3161//             keymap.add_identifier("not_editing");
3162//         }
3163//     }
3164// }
3165
3166impl EventEmitter<PanelEvent> for CollabPanel {}
3167
3168impl Panel for CollabPanel {
3169    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3170        CollaborationPanelSettings::get_global(cx).dock
3171    }
3172
3173    fn position_is_valid(&self, position: DockPosition) -> bool {
3174        matches!(position, DockPosition::Left | DockPosition::Right)
3175    }
3176
3177    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3178        settings::update_settings_file::<CollaborationPanelSettings>(
3179            self.fs.clone(),
3180            cx,
3181            move |settings| settings.dock = Some(position),
3182        );
3183    }
3184
3185    fn size(&self, cx: &gpui::WindowContext) -> f32 {
3186        self.width.map_or_else(
3187            || CollaborationPanelSettings::get_global(cx).default_width,
3188            |width| width.0,
3189        )
3190    }
3191
3192    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3193        self.width = size.map(|s| px(s));
3194        self.serialize(cx);
3195        cx.notify();
3196    }
3197
3198    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
3199        CollaborationPanelSettings::get_global(cx)
3200            .button
3201            .then(|| ui::Icon::Collab)
3202    }
3203
3204    fn toggle_action(&self) -> Box<dyn gpui::Action> {
3205        Box::new(ToggleFocus)
3206    }
3207
3208    fn persistent_name() -> &'static str {
3209        "CollabPanel"
3210    }
3211}
3212
3213impl FocusableView for CollabPanel {
3214    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
3215        self.filter_editor.focus_handle(cx).clone()
3216    }
3217}
3218
3219impl PartialEq for ListEntry {
3220    fn eq(&self, other: &Self) -> bool {
3221        match self {
3222            ListEntry::Header(section_1) => {
3223                if let ListEntry::Header(section_2) = other {
3224                    return section_1 == section_2;
3225                }
3226            }
3227            ListEntry::CallParticipant { user: user_1, .. } => {
3228                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3229                    return user_1.id == user_2.id;
3230                }
3231            }
3232            ListEntry::ParticipantProject {
3233                project_id: project_id_1,
3234                ..
3235            } => {
3236                if let ListEntry::ParticipantProject {
3237                    project_id: project_id_2,
3238                    ..
3239                } = other
3240                {
3241                    return project_id_1 == project_id_2;
3242                }
3243            }
3244            ListEntry::ParticipantScreen {
3245                peer_id: peer_id_1, ..
3246            } => {
3247                if let ListEntry::ParticipantScreen {
3248                    peer_id: peer_id_2, ..
3249                } = other
3250                {
3251                    return peer_id_1 == peer_id_2;
3252                }
3253            }
3254            ListEntry::Channel {
3255                channel: channel_1, ..
3256            } => {
3257                if let ListEntry::Channel {
3258                    channel: channel_2, ..
3259                } = other
3260                {
3261                    return channel_1.id == channel_2.id;
3262                }
3263            }
3264            ListEntry::ChannelNotes { channel_id } => {
3265                if let ListEntry::ChannelNotes {
3266                    channel_id: other_id,
3267                } = other
3268                {
3269                    return channel_id == other_id;
3270                }
3271            }
3272            ListEntry::ChannelChat { channel_id } => {
3273                if let ListEntry::ChannelChat {
3274                    channel_id: other_id,
3275                } = other
3276                {
3277                    return channel_id == other_id;
3278                }
3279            }
3280            // ListEntry::ChannelInvite(channel_1) => {
3281            //     if let ListEntry::ChannelInvite(channel_2) = other {
3282            //         return channel_1.id == channel_2.id;
3283            //     }
3284            // }
3285            ListEntry::IncomingRequest(user_1) => {
3286                if let ListEntry::IncomingRequest(user_2) = other {
3287                    return user_1.id == user_2.id;
3288                }
3289            }
3290            ListEntry::OutgoingRequest(user_1) => {
3291                if let ListEntry::OutgoingRequest(user_2) = other {
3292                    return user_1.id == user_2.id;
3293                }
3294            }
3295            ListEntry::Contact {
3296                contact: contact_1, ..
3297            } => {
3298                if let ListEntry::Contact {
3299                    contact: contact_2, ..
3300                } = other
3301                {
3302                    return contact_1.user.id == contact_2.user.id;
3303                }
3304            }
3305            ListEntry::ChannelEditor { depth } => {
3306                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3307                    return depth == other_depth;
3308                }
3309            }
3310            ListEntry::ContactPlaceholder => {
3311                if let ListEntry::ContactPlaceholder = other {
3312                    return true;
3313                }
3314            }
3315        }
3316        false
3317    }
3318}
3319
3320// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3321//     Svg::new(svg_path)
3322//         .with_color(style.color)
3323//         .constrained()
3324//         .with_width(style.icon_width)
3325//         .aligned()
3326//         .constrained()
3327//         .with_width(style.button_width)
3328//         .with_height(style.button_width)
3329//         .contained()
3330//         .with_style(style.container)
3331// }
3332
3333struct DraggedChannelView {
3334    channel: Channel,
3335    width: Pixels,
3336}
3337
3338impl Render for DraggedChannelView {
3339    type Element = Div;
3340
3341    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3342        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3343        h_stack()
3344            .font(ui_font)
3345            .bg(cx.theme().colors().background)
3346            .w(self.width)
3347            .p_1()
3348            .gap_1()
3349            .child(
3350                IconElement::new(
3351                    if self.channel.visibility == proto::ChannelVisibility::Public {
3352                        Icon::Public
3353                    } else {
3354                        Icon::Hash
3355                    },
3356                )
3357                .size(IconSize::Small)
3358                .color(Color::Muted),
3359            )
3360            .child(Label::new(self.channel.name.clone()))
3361    }
3362}