collab_panel.rs

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