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