collab_panel.rs

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