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().border_1().border_color(gpui::red()).child(
2031            Button::new("sign_in", "Sign in to collaborate").on_click(cx.listener(
2032                |this, _, cx| {
2033                    let client = this.client.clone();
2034                    cx.spawn(|_, mut cx| async move {
2035                        client
2036                            .authenticate_and_connect(true, &cx)
2037                            .await
2038                            .notify_async_err(&mut cx);
2039                    })
2040                    .detach()
2041                },
2042            )),
2043        )
2044    }
2045
2046    fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
2047        let entry = &self.entries[ix];
2048
2049        let is_selected = self.selection == Some(ix);
2050        match entry {
2051            ListEntry::Header(section) => {
2052                let is_collapsed = self.collapsed_sections.contains(section);
2053                self.render_header(*section, is_selected, is_collapsed, cx)
2054                    .into_any_element()
2055            }
2056            ListEntry::Contact { contact, calling } => self
2057                .render_contact(contact, *calling, is_selected, cx)
2058                .into_any_element(),
2059            ListEntry::ContactPlaceholder => self
2060                .render_contact_placeholder(is_selected, cx)
2061                .into_any_element(),
2062            ListEntry::IncomingRequest(user) => self
2063                .render_contact_request(user, true, is_selected, cx)
2064                .into_any_element(),
2065            ListEntry::OutgoingRequest(user) => self
2066                .render_contact_request(user, false, is_selected, cx)
2067                .into_any_element(),
2068            ListEntry::Channel {
2069                channel,
2070                depth,
2071                has_children,
2072            } => self
2073                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
2074                .into_any_element(),
2075            ListEntry::ChannelEditor { depth } => {
2076                self.render_channel_editor(*depth, cx).into_any_element()
2077            }
2078            ListEntry::CallParticipant {
2079                user,
2080                peer_id,
2081                is_pending,
2082            } => self
2083                .render_call_participant(user, *peer_id, *is_pending, cx)
2084                .into_any_element(),
2085            ListEntry::ParticipantProject {
2086                project_id,
2087                worktree_root_names,
2088                host_user_id,
2089                is_last,
2090            } => self
2091                .render_participant_project(
2092                    *project_id,
2093                    &worktree_root_names,
2094                    *host_user_id,
2095                    *is_last,
2096                    cx,
2097                )
2098                .into_any_element(),
2099            ListEntry::ParticipantScreen { peer_id, is_last } => self
2100                .render_participant_screen(*peer_id, *is_last, cx)
2101                .into_any_element(),
2102            ListEntry::ChannelNotes { channel_id } => self
2103                .render_channel_notes(*channel_id, cx)
2104                .into_any_element(),
2105            ListEntry::ChannelChat { channel_id } => {
2106                self.render_channel_chat(*channel_id, cx).into_any_element()
2107            }
2108        }
2109    }
2110
2111    fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
2112        v_stack()
2113            .size_full()
2114            .child(list(self.list_state.clone()).full())
2115            .child(
2116                div().p_2().child(
2117                    div()
2118                        .border_primary(cx)
2119                        .border_t()
2120                        .child(self.filter_editor.clone()),
2121                ),
2122            )
2123    }
2124
2125    fn render_header(
2126        &self,
2127        section: Section,
2128        is_selected: bool,
2129        is_collapsed: bool,
2130        cx: &ViewContext<Self>,
2131    ) -> impl IntoElement {
2132        let mut channel_link = None;
2133        let mut channel_tooltip_text = None;
2134        let mut channel_icon = None;
2135        // let mut is_dragged_over = false;
2136
2137        let text = match section {
2138            Section::ActiveCall => {
2139                let channel_name = maybe!({
2140                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
2141
2142                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
2143
2144                    channel_link = Some(channel.link());
2145                    (channel_icon, channel_tooltip_text) = match channel.visibility {
2146                        proto::ChannelVisibility::Public => {
2147                            (Some("icons/public.svg"), Some("Copy public channel link."))
2148                        }
2149                        proto::ChannelVisibility::Members => {
2150                            (Some("icons/hash.svg"), Some("Copy private channel link."))
2151                        }
2152                    };
2153
2154                    Some(channel.name.as_ref())
2155                });
2156
2157                if let Some(name) = channel_name {
2158                    SharedString::from(format!("{}", name))
2159                } else {
2160                    SharedString::from("Current Call")
2161                }
2162            }
2163            Section::ContactRequests => SharedString::from("Requests"),
2164            Section::Contacts => SharedString::from("Contacts"),
2165            Section::Channels => SharedString::from("Channels"),
2166            Section::ChannelInvites => SharedString::from("Invites"),
2167            Section::Online => SharedString::from("Online"),
2168            Section::Offline => SharedString::from("Offline"),
2169        };
2170
2171        let button = match section {
2172            Section::ActiveCall => channel_link.map(|channel_link| {
2173                let channel_link_copy = channel_link.clone();
2174                IconButton::new("channel-link", Icon::Copy)
2175                    .icon_size(IconSize::Small)
2176                    .size(ButtonSize::None)
2177                    .visible_on_hover("section-header")
2178                    .on_click(move |_, cx| {
2179                        let item = ClipboardItem::new(channel_link_copy.clone());
2180                        cx.write_to_clipboard(item)
2181                    })
2182                    .tooltip(|cx| Tooltip::text("Copy channel link", cx))
2183                    .into_any_element()
2184            }),
2185            Section::Contacts => Some(
2186                div()
2187                    .border_1()
2188                    .border_color(gpui::red())
2189                    .child(
2190                        IconButton::new("add-contact", Icon::Plus)
2191                            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2192                            .tooltip(|cx| Tooltip::text("Search for new contact", cx)),
2193                    )
2194                    .into_any_element(),
2195            ),
2196            Section::Channels => Some(
2197                IconButton::new("add-channel", Icon::Plus)
2198                    .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
2199                    .tooltip(|cx| Tooltip::text("Create a channel", cx))
2200                    .into_any_element(),
2201            ),
2202            _ => None,
2203        };
2204
2205        let can_collapse = match section {
2206            Section::ActiveCall | Section::Channels | Section::Contacts => false,
2207            Section::ChannelInvites
2208            | Section::ContactRequests
2209            | Section::Online
2210            | Section::Offline => true,
2211        };
2212
2213        let mut row = h_stack()
2214            .w_full()
2215            .group("section-header")
2216            .child(
2217                ListHeader::new(text)
2218                    .toggle(if can_collapse {
2219                        Some(!is_collapsed)
2220                    } else {
2221                        None
2222                    })
2223                    .inset(true)
2224                    .end_slot::<AnyElement>(button)
2225                    .selected(is_selected),
2226            )
2227            .when(section == Section::Channels, |el| {
2228                el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
2229                    .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
2230                        this.channel_store
2231                            .update(cx, |channel_store, cx| {
2232                                channel_store.move_channel(dragged_channel.id, None, cx)
2233                            })
2234                            .detach_and_log_err(cx)
2235                    }))
2236            });
2237
2238        if section == Section::Offline {
2239            row = div().border_1().border_color(gpui::red()).child(row);
2240        }
2241
2242        row
2243    }
2244
2245    fn render_contact(
2246        &self,
2247        contact: &Contact,
2248        calling: bool,
2249        is_selected: bool,
2250        cx: &mut ViewContext<Self>,
2251    ) -> impl IntoElement {
2252        let online = contact.online;
2253        let busy = contact.busy || calling;
2254        let user_id = contact.user.id;
2255        let github_login = SharedString::from(contact.user.github_login.clone());
2256        let mut item =
2257            ListItem::new(github_login.clone())
2258                .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2259                .child(
2260                    h_stack()
2261                        .w_full()
2262                        .justify_between()
2263                        .child(Label::new(github_login.clone()))
2264                        .when(calling, |el| {
2265                            el.child(Label::new("Calling").color(Color::Muted))
2266                        })
2267                        .when(!calling, |el| {
2268                            el.child(
2269                                IconButton::new("remove_contact", Icon::Close)
2270                                    .icon_color(Color::Muted)
2271                                    .visible_on_hover("")
2272                                    .tooltip(|cx| Tooltip::text("Remove Contact", cx))
2273                                    .on_click(cx.listener({
2274                                        let github_login = github_login.clone();
2275                                        move |this, _, cx| {
2276                                            this.remove_contact(user_id, &github_login, cx);
2277                                        }
2278                                    })),
2279                            )
2280                        }),
2281                )
2282                .start_slot(
2283                    // todo!() handle contacts with no avatar
2284                    Avatar::new(contact.user.avatar_uri.clone())
2285                        .availability_indicator(if online { Some(!busy) } else { None }),
2286                )
2287                .when(online && !busy, |el| {
2288                    el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
2289                });
2290
2291        div()
2292            .id(github_login.clone())
2293            .group("")
2294            .child(item)
2295            .tooltip(move |cx| {
2296                let text = if !online {
2297                    format!(" {} is offline", &github_login)
2298                } else if busy {
2299                    format!(" {} is on a call", &github_login)
2300                } else {
2301                    let room = ActiveCall::global(cx).read(cx).room();
2302                    if room.is_some() {
2303                        format!("Invite {} to join call", &github_login)
2304                    } else {
2305                        format!("Call {}", &github_login)
2306                    }
2307                };
2308                Tooltip::text(text, cx)
2309            })
2310    }
2311
2312    fn render_contact_request(
2313        &self,
2314        user: &Arc<User>,
2315        is_incoming: bool,
2316        is_selected: bool,
2317        cx: &mut ViewContext<Self>,
2318    ) -> impl IntoElement {
2319        let github_login = SharedString::from(user.github_login.clone());
2320        let user_id = user.id;
2321        let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2322        let color = if is_contact_request_pending {
2323            Color::Muted
2324        } else {
2325            Color::Default
2326        };
2327
2328        let controls = if is_incoming {
2329            vec![
2330                IconButton::new("remove_contact", Icon::Close)
2331                    .on_click(cx.listener(move |this, _, cx| {
2332                        this.respond_to_contact_request(user_id, false, cx);
2333                    }))
2334                    .icon_color(color)
2335                    .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2336                IconButton::new("remove_contact", Icon::Check)
2337                    .on_click(cx.listener(move |this, _, cx| {
2338                        this.respond_to_contact_request(user_id, true, cx);
2339                    }))
2340                    .icon_color(color)
2341                    .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2342            ]
2343        } else {
2344            let github_login = github_login.clone();
2345            vec![IconButton::new("remove_contact", Icon::Close)
2346                .on_click(cx.listener(move |this, _, cx| {
2347                    this.remove_contact(user_id, &github_login, cx);
2348                }))
2349                .icon_color(color)
2350                .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2351        };
2352
2353        ListItem::new(github_login.clone())
2354            .child(
2355                h_stack()
2356                    .w_full()
2357                    .justify_between()
2358                    .child(Label::new(github_login.clone()))
2359                    .child(h_stack().children(controls)),
2360            )
2361            .start_slot(Avatar::new(user.avatar_uri.clone()))
2362    }
2363
2364    fn render_contact_placeholder(
2365        &self,
2366        is_selected: bool,
2367        cx: &mut ViewContext<Self>,
2368    ) -> impl IntoElement {
2369        ListItem::new("contact-placeholder")
2370            .child(IconElement::new(Icon::Plus))
2371            .child(Label::new("Add a Contact"))
2372            .selected(is_selected)
2373            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
2374    }
2375
2376    fn render_channel(
2377        &self,
2378        channel: &Channel,
2379        depth: usize,
2380        has_children: bool,
2381        is_selected: bool,
2382        ix: usize,
2383        cx: &mut ViewContext<Self>,
2384    ) -> impl IntoElement {
2385        let channel_id = channel.id;
2386
2387        let is_active = maybe!({
2388            let call_channel = ActiveCall::global(cx)
2389                .read(cx)
2390                .room()?
2391                .read(cx)
2392                .channel_id()?;
2393            Some(call_channel == channel_id)
2394        })
2395        .unwrap_or(false);
2396        let is_public = self
2397            .channel_store
2398            .read(cx)
2399            .channel_for_id(channel_id)
2400            .map(|channel| channel.visibility)
2401            == Some(proto::ChannelVisibility::Public);
2402        let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2403        let disclosed =
2404            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2405
2406        let has_messages_notification = channel.unseen_message_id.is_some();
2407        let has_notes_notification = channel.unseen_note_version.is_some();
2408
2409        const FACEPILE_LIMIT: usize = 3;
2410        let participants = self.channel_store.read(cx).channel_participants(channel_id);
2411
2412        let face_pile = if !participants.is_empty() {
2413            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2414            let user = &participants[0];
2415
2416            let result = FacePile {
2417                faces: participants
2418                    .iter()
2419                    .filter_map(|user| {
2420                        Some(Avatar::new(user.avatar_uri.clone()).into_any_element())
2421                    })
2422                    .take(FACEPILE_LIMIT)
2423                    .chain(if extra_count > 0 {
2424                        // todo!() @nate - this label looks wrong.
2425                        Some(Label::new(format!("+{}", extra_count)).into_any_element())
2426                    } else {
2427                        None
2428                    })
2429                    .collect::<SmallVec<_>>(),
2430            };
2431
2432            Some(result)
2433        } else {
2434            None
2435        };
2436
2437        let width = self.width.unwrap_or(px(240.));
2438
2439        div()
2440            .id(channel_id as usize)
2441            .group("")
2442            .flex()
2443            .w_full()
2444            .on_drag(channel.clone(), move |channel, cx| {
2445                cx.build_view(|cx| DraggedChannelView {
2446                    channel: channel.clone(),
2447                    width,
2448                })
2449            })
2450            .drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
2451            .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
2452                this.channel_store
2453                    .update(cx, |channel_store, cx| {
2454                        channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2455                    })
2456                    .detach_and_log_err(cx)
2457            }))
2458            .child(
2459                ListItem::new(channel_id as usize)
2460                    // Offset the indent depth by one to give us room to show the disclosure.
2461                    .indent_level(depth + 1)
2462                    .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to  step over the disclosure toggle
2463                    .selected(is_selected || is_active)
2464                    .toggle(disclosed)
2465                    .on_toggle(
2466                        cx.listener(move |this, _, cx| {
2467                            this.toggle_channel_collapsed(channel_id, cx)
2468                        }),
2469                    )
2470                    .on_click(cx.listener(move |this, _, cx| {
2471                        if this.drag_target_channel == ChannelDragTarget::None {
2472                            if is_active {
2473                                this.open_channel_notes(channel_id, cx)
2474                            } else {
2475                                this.join_channel(channel_id, cx)
2476                            }
2477                        }
2478                    }))
2479                    .on_secondary_mouse_down(cx.listener(
2480                        move |this, event: &MouseDownEvent, cx| {
2481                            this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
2482                        },
2483                    ))
2484                    .start_slot(
2485                        IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
2486                            .size(IconSize::Small)
2487                            .color(Color::Muted),
2488                    )
2489                    .child(
2490                        h_stack()
2491                            .id(channel_id as usize)
2492                            .child(Label::new(channel.name.clone()))
2493                            .children(face_pile.map(|face_pile| face_pile.render(cx))),
2494                    )
2495                    .end_slot(
2496                        h_stack()
2497                            .child(
2498                                IconButton::new("channel_chat", Icon::MessageBubbles)
2499                                    .icon_color(if has_messages_notification {
2500                                        Color::Default
2501                                    } else {
2502                                        Color::Muted
2503                                    })
2504                                    .when(!has_messages_notification, |this| {
2505                                        this.visible_on_hover("")
2506                                    })
2507                                    .on_click(cx.listener(move |this, _, cx| {
2508                                        this.join_channel_chat(channel_id, cx)
2509                                    }))
2510                                    .tooltip(|cx| Tooltip::text("Open channel chat", cx)),
2511                            )
2512                            .child(
2513                                IconButton::new("channel_notes", Icon::File)
2514                                    .icon_color(if has_notes_notification {
2515                                        Color::Default
2516                                    } else {
2517                                        Color::Muted
2518                                    })
2519                                    .when(!has_notes_notification, |this| this.visible_on_hover(""))
2520                                    .on_click(cx.listener(move |this, _, cx| {
2521                                        this.open_channel_notes(channel_id, cx)
2522                                    }))
2523                                    .tooltip(|cx| Tooltip::text("Open channel notes", cx)),
2524                            ),
2525                    ),
2526            )
2527            .tooltip(|cx| Tooltip::text("Join channel", cx))
2528
2529        // let channel_id = channel.id;
2530        // let collab_theme = &theme.collab_panel;
2531        // let is_public = self
2532        //     .channel_store
2533        //     .read(cx)
2534        //     .channel_for_id(channel_id)
2535        //     .map(|channel| channel.visibility)
2536        //     == Some(proto::ChannelVisibility::Public);
2537        // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
2538        // let disclosed =
2539        //     has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
2540
2541        // enum ChannelCall {}
2542        // enum ChannelNote {}
2543        // enum NotesTooltip {}
2544        // enum ChatTooltip {}
2545        // enum ChannelTooltip {}
2546
2547        // let mut is_dragged_over = false;
2548        // if cx
2549        //     .global::<DragAndDrop<Workspace>>()
2550        //     .currently_dragged::<Channel>(cx.window())
2551        //     .is_some()
2552        //     && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
2553        // {
2554        //     is_dragged_over = true;
2555        // }
2556
2557        // let has_messages_notification = channel.unseen_message_id.is_some();
2558
2559        // MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
2560        //     let row_hovered = state.hovered();
2561
2562        //     let mut select_state = |interactive: &Interactive<ContainerStyle>| {
2563        //         if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
2564        //             interactive.clicked.as_ref().unwrap().clone()
2565        //         } else if state.hovered() || other_selected {
2566        //             interactive
2567        //                 .hovered
2568        //                 .as_ref()
2569        //                 .unwrap_or(&interactive.default)
2570        //                 .clone()
2571        //         } else {
2572        //             interactive.default.clone()
2573        //         }
2574        //     };
2575
2576        //     Flex::<Self>::row()
2577        //         .with_child(
2578        //             Svg::new(if is_public {
2579        //                 "icons/public.svg"
2580        //             } else {
2581        //                 "icons/hash.svg"
2582        //             })
2583        //             .with_color(collab_theme.channel_hash.color)
2584        //             .constrained()
2585        //             .with_width(collab_theme.channel_hash.width)
2586        //             .aligned()
2587        //             .left(),
2588        //         )
2589        //         .with_child({
2590        //             let style = collab_theme.channel_name.inactive_state();
2591        //             Flex::row()
2592        //                 .with_child(
2593        //                     Label::new(channel.name.clone(), style.text.clone())
2594        //                         .contained()
2595        //                         .with_style(style.container)
2596        //                         .aligned()
2597        //                         .left()
2598        //                         .with_tooltip::<ChannelTooltip>(
2599        //                             ix,
2600        //                             "Join channel",
2601        //                             None,
2602        //                             theme.tooltip.clone(),
2603        //                             cx,
2604        //                         ),
2605        //                 )
2606        //                 .with_children({
2607        //                     let participants =
2608        //                         self.channel_store.read(cx).channel_participants(channel_id);
2609
2610        //                     if !participants.is_empty() {
2611        //                         let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2612
2613        //                         let result = FacePile::new(collab_theme.face_overlap)
2614        //                             .with_children(
2615        //                                 participants
2616        //                                     .iter()
2617        //                                     .filter_map(|user| {
2618        //                                         Some(
2619        //                                             Image::from_data(user.avatar.clone()?)
2620        //                                                 .with_style(collab_theme.channel_avatar),
2621        //                                         )
2622        //                                     })
2623        //                                     .take(FACEPILE_LIMIT),
2624        //                             )
2625        //                             .with_children((extra_count > 0).then(|| {
2626        //                                 Label::new(
2627        //                                     format!("+{}", extra_count),
2628        //                                     collab_theme.extra_participant_label.text.clone(),
2629        //                                 )
2630        //                                 .contained()
2631        //                                 .with_style(collab_theme.extra_participant_label.container)
2632        //                             }));
2633
2634        //                         Some(result)
2635        //                     } else {
2636        //                         None
2637        //                     }
2638        //                 })
2639        //                 .with_spacing(8.)
2640        //                 .align_children_center()
2641        //                 .flex(1., true)
2642        //         })
2643        //         .with_child(
2644        //             MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
2645        //                 let container_style = collab_theme
2646        //                     .disclosure
2647        //                     .button
2648        //                     .style_for(mouse_state)
2649        //                     .container;
2650
2651        //                 if channel.unseen_message_id.is_some() {
2652        //                     Svg::new("icons/conversations.svg")
2653        //                         .with_color(collab_theme.channel_note_active_color)
2654        //                         .constrained()
2655        //                         .with_width(collab_theme.channel_hash.width)
2656        //                         .contained()
2657        //                         .with_style(container_style)
2658        //                         .with_uniform_padding(4.)
2659        //                         .into_any()
2660        //                 } else if row_hovered {
2661        //                     Svg::new("icons/conversations.svg")
2662        //                         .with_color(collab_theme.channel_hash.color)
2663        //                         .constrained()
2664        //                         .with_width(collab_theme.channel_hash.width)
2665        //                         .contained()
2666        //                         .with_style(container_style)
2667        //                         .with_uniform_padding(4.)
2668        //                         .into_any()
2669        //                 } else {
2670        //                     Empty::new().into_any()
2671        //                 }
2672        //             })
2673        //             .on_click(MouseButton::Left, move |_, this, cx| {
2674        //                 this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
2675        //             })
2676        //             .with_tooltip::<ChatTooltip>(
2677        //                 ix,
2678        //                 "Open channel chat",
2679        //                 None,
2680        //                 theme.tooltip.clone(),
2681        //                 cx,
2682        //             )
2683        //             .contained()
2684        //             .with_margin_right(4.),
2685        //         )
2686        //         .with_child(
2687        //             MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
2688        //                 let container_style = collab_theme
2689        //                     .disclosure
2690        //                     .button
2691        //                     .style_for(mouse_state)
2692        //                     .container;
2693        //                 if row_hovered || channel.unseen_note_version.is_some() {
2694        //                     Svg::new("icons/file.svg")
2695        //                         .with_color(if channel.unseen_note_version.is_some() {
2696        //                             collab_theme.channel_note_active_color
2697        //                         } else {
2698        //                             collab_theme.channel_hash.color
2699        //                         })
2700        //                         .constrained()
2701        //                         .with_width(collab_theme.channel_hash.width)
2702        //                         .contained()
2703        //                         .with_style(container_style)
2704        //                         .with_uniform_padding(4.)
2705        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2706        //                         .with_tooltip::<NotesTooltip>(
2707        //                             ix as usize,
2708        //                             "Open channel notes",
2709        //                             None,
2710        //                             theme.tooltip.clone(),
2711        //                             cx,
2712        //                         )
2713        //                         .into_any()
2714        //                 } else if has_messages_notification {
2715        //                     Empty::new()
2716        //                         .constrained()
2717        //                         .with_width(collab_theme.channel_hash.width)
2718        //                         .contained()
2719        //                         .with_uniform_padding(4.)
2720        //                         .with_margin_right(collab_theme.channel_hash.container.margin.left)
2721        //                         .into_any()
2722        //                 } else {
2723        //                     Empty::new().into_any()
2724        //                 }
2725        //             })
2726        //             .on_click(MouseButton::Left, move |_, this, cx| {
2727        //                 this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2728        //             }),
2729        //         )
2730        //         .align_children_center()
2731        //         .styleable_component()
2732        //         .disclosable(
2733        //             disclosed,
2734        //             Box::new(ToggleCollapse {
2735        //                 location: channel.id.clone(),
2736        //             }),
2737        //         )
2738        //         .with_id(ix)
2739        //         .with_style(collab_theme.disclosure.clone())
2740        //         .element()
2741        //         .constrained()
2742        //         .with_height(collab_theme.row_height)
2743        //         .contained()
2744        //         .with_style(select_state(
2745        //             collab_theme
2746        //                 .channel_row
2747        //                 .in_state(is_selected || is_active || is_dragged_over),
2748        //         ))
2749        //         .with_padding_left(
2750        //             collab_theme.channel_row.default_style().padding.left
2751        //                 + collab_theme.channel_indent * depth as f32,
2752        //         )
2753        // })
2754        // .on_click(MouseButton::Left, move |_, this, cx| {
2755        //     if this.
2756        // drag_target_channel == ChannelDragTarget::None {
2757        //         if is_active {
2758        //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
2759        //         } else {
2760        //             this.join_channel(channel_id, cx)
2761        //         }
2762        //     }
2763        // })
2764        // .on_click(MouseButton::Right, {
2765        //     let channel = channel.clone();
2766        //     move |e, this, cx| {
2767        //         this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
2768        //     }
2769        // })
2770        // .on_up(MouseButton::Left, move |_, this, cx| {
2771        //     if let Some((_, dragged_channel)) = cx
2772        //         .global::<DragAndDrop<Workspace>>()
2773        //         .currently_dragged::<Channel>(cx.window())
2774        //     {
2775        //         this.channel_store
2776        //             .update(cx, |channel_store, cx| {
2777        //                 channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
2778        //             })
2779        //             .detach_and_log_err(cx)
2780        //     }
2781        // })
2782        // .on_move({
2783        //     let channel = channel.clone();
2784        //     move |_, this, cx| {
2785        //         if let Some((_, dragged_channel)) = cx
2786        //             .global::<DragAndDrop<Workspace>>()
2787        //             .currently_dragged::<Channel>(cx.window())
2788        //         {
2789        //             if channel.id != dragged_channel.id {
2790        //                 this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
2791        //             }
2792        //             cx.notify()
2793        //         }
2794        //     }
2795        // })
2796        // .as_draggable::<_, Channel>(
2797        //     channel.clone(),
2798        //     move |_, channel, cx: &mut ViewContext<Workspace>| {
2799        //         let theme = &theme::current(cx).collab_panel;
2800
2801        //         Flex::<Workspace>::row()
2802        //             .with_child(
2803        //                 Svg::new("icons/hash.svg")
2804        //                     .with_color(theme.channel_hash.color)
2805        //                     .constrained()
2806        //                     .with_width(theme.channel_hash.width)
2807        //                     .aligned()
2808        //                     .left(),
2809        //             )
2810        //             .with_child(
2811        //                 Label::new(channel.name.clone(), theme.channel_name.text.clone())
2812        //                     .contained()
2813        //                     .with_style(theme.channel_name.container)
2814        //                     .aligned()
2815        //                     .left(),
2816        //             )
2817        //             .align_children_center()
2818        //             .contained()
2819        //             .with_background_color(
2820        //                 theme
2821        //                     .container
2822        //                     .background_color
2823        //                     .unwrap_or(gpui::color::Color::transparent_black()),
2824        //             )
2825        //             .contained()
2826        //             .with_padding_left(
2827        //                 theme.channel_row.default_style().padding.left
2828        //                     + theme.channel_indent * depth as f32,
2829        //             )
2830        //             .into_any()
2831        //     },
2832        // )
2833        // .with_cursor_style(CursorStyle::PointingHand)
2834        // .into_any()
2835    }
2836
2837    fn render_channel_editor(&self, depth: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
2838        let item = ListItem::new("channel-editor")
2839            .inset(false)
2840            .indent_level(depth)
2841            .start_slot(
2842                IconElement::new(Icon::Hash)
2843                    .size(IconSize::Small)
2844                    .color(Color::Muted),
2845            );
2846
2847        if let Some(pending_name) = self
2848            .channel_editing_state
2849            .as_ref()
2850            .and_then(|state| state.pending_name())
2851        {
2852            item.child(Label::new(pending_name))
2853        } else {
2854            item.child(
2855                div()
2856                    .w_full()
2857                    .py_1() // todo!() @nate this is a px off at the default font size.
2858                    .child(self.channel_name_editor.clone()),
2859            )
2860        }
2861    }
2862}
2863
2864fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
2865    let rem_size = cx.rem_size();
2866    let line_height = cx.text_style().line_height_in_pixels(rem_size);
2867    let width = rem_size * 1.5;
2868    let thickness = px(2.);
2869    let color = cx.theme().colors().text;
2870
2871    canvas(move |bounds, cx| {
2872        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
2873        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
2874        let right = bounds.right();
2875        let top = bounds.top();
2876
2877        cx.paint_quad(fill(
2878            Bounds::from_corners(
2879                point(start_x, top),
2880                point(
2881                    start_x + thickness,
2882                    if is_last { start_y } else { bounds.bottom() },
2883                ),
2884            ),
2885            color,
2886        ));
2887        cx.paint_quad(fill(
2888            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
2889            color,
2890        ));
2891    })
2892    .w(width)
2893    .h(line_height)
2894}
2895
2896impl Render for CollabPanel {
2897    type Element = Focusable<Div>;
2898
2899    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
2900        v_stack()
2901            .key_context("CollabPanel")
2902            .on_action(cx.listener(CollabPanel::cancel))
2903            .on_action(cx.listener(CollabPanel::select_next))
2904            .on_action(cx.listener(CollabPanel::select_prev))
2905            .on_action(cx.listener(CollabPanel::confirm))
2906            .on_action(cx.listener(CollabPanel::insert_space))
2907            //     .on_action(cx.listener(CollabPanel::remove))
2908            .on_action(cx.listener(CollabPanel::remove_selected_channel))
2909            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
2910            //     .on_action(cx.listener(CollabPanel::new_subchannel))
2911            //     .on_action(cx.listener(CollabPanel::invite_members))
2912            //     .on_action(cx.listener(CollabPanel::manage_members))
2913            .on_action(cx.listener(CollabPanel::rename_selected_channel))
2914            //     .on_action(cx.listener(CollabPanel::rename_channel))
2915            //     .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action))
2916            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
2917            .on_action(cx.listener(CollabPanel::expand_selected_channel))
2918            //     .on_action(cx.listener(CollabPanel::open_channel_notes))
2919            //     .on_action(cx.listener(CollabPanel::join_channel_chat))
2920            //     .on_action(cx.listener(CollabPanel::copy_channel_link))
2921            .track_focus(&self.focus_handle)
2922            .size_full()
2923            .child(if self.user_store.read(cx).current_user().is_none() {
2924                self.render_signed_out(cx)
2925            } else {
2926                self.render_signed_in(cx)
2927            })
2928            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2929                overlay()
2930                    .position(*position)
2931                    .anchor(gpui::AnchorCorner::TopLeft)
2932                    .child(menu.clone())
2933            }))
2934    }
2935}
2936
2937// impl View for CollabPanel {
2938//     fn ui_name() -> &'static str {
2939//         "CollabPanel"
2940//     }
2941
2942//     fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
2943//         if !self.has_focus {
2944//             self.has_focus = true;
2945//             if !self.context_menu.is_focused(cx) {
2946//                 if let Some(editing_state) = &self.channel_editing_state {
2947//                     if editing_state.pending_name().is_none() {
2948//                         cx.focus(&self.channel_name_editor);
2949//                     } else {
2950//                         cx.focus(&self.filter_editor);
2951//                     }
2952//                 } else {
2953//                     cx.focus(&self.filter_editor);
2954//                 }
2955//             }
2956//             cx.emit(Event::Focus);
2957//         }
2958//     }
2959
2960//     fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
2961//         self.has_focus = false;
2962//     }
2963
2964//     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
2965//         let theme = &theme::current(cx).collab_panel;
2966
2967//         if self.user_store.read(cx).current_user().is_none() {
2968//             enum LogInButton {}
2969
2970//             return Flex::column()
2971//                 .with_child(
2972//                     MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
2973//                         let button = theme.log_in_button.style_for(state);
2974//                         Label::new("Sign in to collaborate", button.text.clone())
2975//                             .aligned()
2976//                             .left()
2977//                             .contained()
2978//                             .with_style(button.container)
2979//                     })
2980//                     .on_click(MouseButton::Left, |_, this, cx| {
2981//                         let client = this.client.clone();
2982//                         cx.spawn(|_, cx| async move {
2983//                             client.authenticate_and_connect(true, &cx).await.log_err();
2984//                         })
2985//                         .detach();
2986//                     })
2987//                     .with_cursor_style(CursorStyle::PointingHand),
2988//                 )
2989//                 .contained()
2990//                 .with_style(theme.container)
2991//                 .into_any();
2992//         }
2993
2994//         enum PanelFocus {}
2995//         MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
2996//             Stack::new()
2997//                 .with_child(
2998//                     Flex::column()
2999//                         .with_child(
3000//                             Flex::row().with_child(
3001//                                 ChildView::new(&self.filter_editor, cx)
3002//                                     .contained()
3003//                                     .with_style(theme.user_query_editor.container)
3004//                                     .flex(1.0, true),
3005//                             ),
3006//                         )
3007//                         .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3008//                         .contained()
3009//                         .with_style(theme.container)
3010//                         .into_any(),
3011//                 )
3012//                 .with_children(
3013//                     (!self.context_menu_on_selected)
3014//                         .then(|| ChildView::new(&self.context_menu, cx)),
3015//                 )
3016//                 .into_any()
3017//         })
3018//         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3019//         .into_any_named("collab panel")
3020//     }
3021
3022//     fn update_keymap_context(
3023//         &self,
3024//         keymap: &mut gpui::keymap_matcher::KeymapContext,
3025//         _: &AppContext,
3026//     ) {
3027//         Self::reset_to_default_keymap_context(keymap);
3028//         if self.channel_editing_state.is_some() {
3029//             keymap.add_identifier("editing");
3030//         } else {
3031//             keymap.add_identifier("not_editing");
3032//         }
3033//     }
3034// }
3035
3036impl EventEmitter<PanelEvent> for CollabPanel {}
3037
3038impl Panel for CollabPanel {
3039    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3040        CollaborationPanelSettings::get_global(cx).dock
3041    }
3042
3043    fn position_is_valid(&self, position: DockPosition) -> bool {
3044        matches!(position, DockPosition::Left | DockPosition::Right)
3045    }
3046
3047    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3048        settings::update_settings_file::<CollaborationPanelSettings>(
3049            self.fs.clone(),
3050            cx,
3051            move |settings| settings.dock = Some(position),
3052        );
3053    }
3054
3055    fn size(&self, cx: &gpui::WindowContext) -> f32 {
3056        self.width.map_or_else(
3057            || CollaborationPanelSettings::get_global(cx).default_width,
3058            |width| width.0,
3059        )
3060    }
3061
3062    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3063        self.width = size.map(|s| px(s));
3064        self.serialize(cx);
3065        cx.notify();
3066    }
3067
3068    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
3069        CollaborationPanelSettings::get_global(cx)
3070            .button
3071            .then(|| ui::Icon::Collab)
3072    }
3073
3074    fn toggle_action(&self) -> Box<dyn gpui::Action> {
3075        Box::new(ToggleFocus)
3076    }
3077
3078    fn persistent_name() -> &'static str {
3079        "CollabPanel"
3080    }
3081}
3082
3083impl FocusableView for CollabPanel {
3084    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
3085        self.filter_editor.focus_handle(cx).clone()
3086    }
3087}
3088
3089impl PartialEq for ListEntry {
3090    fn eq(&self, other: &Self) -> bool {
3091        match self {
3092            ListEntry::Header(section_1) => {
3093                if let ListEntry::Header(section_2) = other {
3094                    return section_1 == section_2;
3095                }
3096            }
3097            ListEntry::CallParticipant { user: user_1, .. } => {
3098                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3099                    return user_1.id == user_2.id;
3100                }
3101            }
3102            ListEntry::ParticipantProject {
3103                project_id: project_id_1,
3104                ..
3105            } => {
3106                if let ListEntry::ParticipantProject {
3107                    project_id: project_id_2,
3108                    ..
3109                } = other
3110                {
3111                    return project_id_1 == project_id_2;
3112                }
3113            }
3114            ListEntry::ParticipantScreen {
3115                peer_id: peer_id_1, ..
3116            } => {
3117                if let ListEntry::ParticipantScreen {
3118                    peer_id: peer_id_2, ..
3119                } = other
3120                {
3121                    return peer_id_1 == peer_id_2;
3122                }
3123            }
3124            ListEntry::Channel {
3125                channel: channel_1, ..
3126            } => {
3127                if let ListEntry::Channel {
3128                    channel: channel_2, ..
3129                } = other
3130                {
3131                    return channel_1.id == channel_2.id;
3132                }
3133            }
3134            ListEntry::ChannelNotes { channel_id } => {
3135                if let ListEntry::ChannelNotes {
3136                    channel_id: other_id,
3137                } = other
3138                {
3139                    return channel_id == other_id;
3140                }
3141            }
3142            ListEntry::ChannelChat { channel_id } => {
3143                if let ListEntry::ChannelChat {
3144                    channel_id: other_id,
3145                } = other
3146                {
3147                    return channel_id == other_id;
3148                }
3149            }
3150            // ListEntry::ChannelInvite(channel_1) => {
3151            //     if let ListEntry::ChannelInvite(channel_2) = other {
3152            //         return channel_1.id == channel_2.id;
3153            //     }
3154            // }
3155            ListEntry::IncomingRequest(user_1) => {
3156                if let ListEntry::IncomingRequest(user_2) = other {
3157                    return user_1.id == user_2.id;
3158                }
3159            }
3160            ListEntry::OutgoingRequest(user_1) => {
3161                if let ListEntry::OutgoingRequest(user_2) = other {
3162                    return user_1.id == user_2.id;
3163                }
3164            }
3165            ListEntry::Contact {
3166                contact: contact_1, ..
3167            } => {
3168                if let ListEntry::Contact {
3169                    contact: contact_2, ..
3170                } = other
3171                {
3172                    return contact_1.user.id == contact_2.user.id;
3173                }
3174            }
3175            ListEntry::ChannelEditor { depth } => {
3176                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3177                    return depth == other_depth;
3178                }
3179            }
3180            ListEntry::ContactPlaceholder => {
3181                if let ListEntry::ContactPlaceholder = other {
3182                    return true;
3183                }
3184            }
3185        }
3186        false
3187    }
3188}
3189
3190// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3191//     Svg::new(svg_path)
3192//         .with_color(style.color)
3193//         .constrained()
3194//         .with_width(style.icon_width)
3195//         .aligned()
3196//         .constrained()
3197//         .with_width(style.button_width)
3198//         .with_height(style.button_width)
3199//         .contained()
3200//         .with_style(style.container)
3201// }
3202
3203struct DraggedChannelView {
3204    channel: Channel,
3205    width: Pixels,
3206}
3207
3208impl Render for DraggedChannelView {
3209    type Element = Div;
3210
3211    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3212        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
3213        h_stack()
3214            .font(ui_font)
3215            .bg(cx.theme().colors().background)
3216            .w(self.width)
3217            .p_1()
3218            .gap_1()
3219            .child(
3220                IconElement::new(
3221                    if self.channel.visibility == proto::ChannelVisibility::Public {
3222                        Icon::Public
3223                    } else {
3224                        Icon::Hash
3225                    },
3226                )
3227                .size(IconSize::Small)
3228                .color(Color::Muted),
3229            )
3230            .child(Label::new(self.channel.name.clone()))
3231    }
3232}