#![allow(unused)] mod channel_modal; mod contact_finder; // use crate::{ // channel_view::{self, ChannelView}, // chat_panel::ChatPanel, // face_pile::FacePile, // panel_settings, CollaborationPanelSettings, // }; // use anyhow::Result; // use call::ActiveCall; // use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; // use channel_modal::ChannelModal; // use client::{ // proto::{self, PeerId}, // Client, Contact, User, UserStore, // }; use contact_finder::ContactFinder; use menu::{Cancel, Confirm, SelectNext, SelectPrev}; use rpc::proto::{self, PeerId}; use smallvec::SmallVec; use theme::{ActiveTheme, ThemeSettings}; // use context_menu::{ContextMenu, ContextMenuItem}; // use db::kvp::KEY_VALUE_STORE; // use drag_and_drop::{DragAndDrop, Draggable}; // use editor::{Cancel, Editor}; // use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; // use futures::StreamExt; // use fuzzy::{match_strings, StringMatchCandidate}; // use gpui::{ // actions, // elements::{ // Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset, // ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, // SafeStylable, Stack, Svg, // }, // fonts::TextStyle, // geometry::{ // rect::RectF, // vector::{vec2f, Vector2F}, // }, // impl_actions, // platform::{CursorStyle, MouseButton, PromptLevel}, // serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache, // ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, // }; // use menu::{Confirm, SelectNext, SelectPrev}; // use project::{Fs, Project}; // use serde_derive::{Deserialize, Serialize}; // use settings::SettingsStore; // use std::{borrow::Cow, hash::Hash, mem, sync::Arc}; // use theme::{components::ComponentExt, IconButton, Interactive}; // use util::{maybe, ResultExt, TryFutureExt}; // use workspace::{ // dock::{DockPosition, Panel}, // item::ItemHandle, // FollowNextCollaborator, Workspace, // }; // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // struct ToggleCollapse { // location: ChannelId, // } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // struct NewChannel { // location: ChannelId, // } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // struct RenameChannel { // channel_id: ChannelId, // } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // struct ToggleSelectedIx { // ix: usize, // } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // struct RemoveChannel { // channel_id: ChannelId, // } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // struct InviteMembers { // channel_id: ChannelId, // } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // struct ManageMembers { // channel_id: ChannelId, // } #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct OpenChannelNotes { pub channel_id: ChannelId, } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // pub struct JoinChannelCall { // pub channel_id: u64, // } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // pub struct JoinChannelChat { // pub channel_id: u64, // } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // pub struct CopyChannelLink { // pub channel_id: u64, // } // #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // struct StartMoveChannelFor { // channel_id: ChannelId, // } // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // struct MoveChannel { // to: ChannelId, // } impl_actions!(collab_panel, [OpenChannelNotes]); actions!( collab_panel, [ ToggleFocus, Remove, Secondary, CollapseSelectedChannel, ExpandSelectedChannel, StartMoveChannel, MoveSelected, InsertSpace, ] ); // impl_actions!( // collab_panel, // [ // RemoveChannel, // NewChannel, // InviteMembers, // ManageMembers, // RenameChannel, // ToggleCollapse, // OpenChannelNotes, // JoinChannelCall, // JoinChannelChat, // CopyChannelLink, // StartMoveChannelFor, // MoveChannel, // ToggleSelectedIx // ] // ); #[derive(Debug, Copy, Clone, PartialEq, Eq)] struct ChannelMoveClipboard { channel_id: ChannelId, } const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; use std::{mem, sync::Arc}; use call::ActiveCall; use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; use client::{Client, Contact, User, UserStore}; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, canvas, div, fill, impl_actions, list, overlay, point, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, ListOffset, ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use ui::prelude::*; use ui::{ h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, Label, ListHeader, ListItem, Tooltip, }; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::NotifyResultExt, Workspace, }; use crate::channel_view::ChannelView; use crate::chat_panel::ChatPanel; use crate::{face_pile::FacePile, CollaborationPanelSettings}; use self::channel_modal::ChannelModal; pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| { workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.toggle_panel_focus::(cx); }); }) .detach(); // contact_finder::init(cx); // channel_modal::init(cx); // channel_view::init(cx); // cx.add_action(CollabPanel::cancel); // cx.add_action(CollabPanel::select_next); // cx.add_action(CollabPanel::select_prev); // cx.add_action(CollabPanel::confirm); // cx.add_action(CollabPanel::insert_space); // cx.add_action(CollabPanel::remove); // cx.add_action(CollabPanel::remove_selected_channel); // cx.add_action(CollabPanel::show_inline_context_menu); // cx.add_action(CollabPanel::new_subchannel); // cx.add_action(CollabPanel::invite_members); // cx.add_action(CollabPanel::manage_members); // cx.add_action(CollabPanel::rename_selected_channel); // cx.add_action(CollabPanel::rename_channel); // cx.add_action(CollabPanel::toggle_channel_collapsed_action); // cx.add_action(CollabPanel::collapse_selected_channel); // cx.add_action(CollabPanel::expand_selected_channel); // cx.add_action(CollabPanel::open_channel_notes); // cx.add_action(CollabPanel::join_channel_chat); // cx.add_action(CollabPanel::copy_channel_link); // cx.add_action( // |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext| { // if panel.selection.take() != Some(action.ix) { // panel.selection = Some(action.ix) // } // cx.notify(); // }, // ); // cx.add_action( // |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext| { // let Some(clipboard) = panel.channel_clipboard.take() else { // return; // }; // let Some(selected_channel) = panel.selected_channel() else { // return; // }; // panel // .channel_store // .update(cx, |channel_store, cx| { // channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx) // }) // .detach_and_log_err(cx) // }, // ); // cx.add_action( // |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext| { // if let Some(clipboard) = panel.channel_clipboard.take() { // panel.channel_store.update(cx, |channel_store, cx| { // channel_store // .move_channel(clipboard.channel_id, Some(action.to), cx) // .detach_and_log_err(cx) // }) // } // }, // ); } #[derive(Debug)] pub enum ChannelEditingState { Create { location: Option, pending_name: Option, }, Rename { location: ChannelId, pending_name: Option, }, } impl ChannelEditingState { fn pending_name(&self) -> Option { match self { ChannelEditingState::Create { pending_name, .. } => pending_name.clone(), ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(), } } } pub struct CollabPanel { width: Option, fs: Arc, focus_handle: FocusHandle, channel_clipboard: Option, pending_serialization: Task>, context_menu: Option<(View, Point, Subscription)>, list_state: ListState, filter_editor: View, channel_name_editor: View, channel_editing_state: Option, entries: Vec, selection: Option, channel_store: Model, user_store: Model, client: Arc, project: Model, match_candidates: Vec, subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, drag_target_channel: ChannelDragTarget, workspace: WeakView, // context_menu_on_selected: bool, } #[derive(PartialEq, Eq)] enum ChannelDragTarget { None, Root, Channel(ChannelId), } #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { width: Option, collapsed_channels: Option>, } // #[derive(Debug)] // pub enum Event { // DockPositionChanged, // Focus, // Dismissed, // } #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] enum Section { ActiveCall, Channels, ChannelInvites, ContactRequests, Contacts, Online, Offline, } #[derive(Clone, Debug)] enum ListEntry { Header(Section), CallParticipant { user: Arc, peer_id: Option, is_pending: bool, }, ParticipantProject { project_id: u64, worktree_root_names: Vec, host_user_id: u64, is_last: bool, }, ParticipantScreen { peer_id: Option, is_last: bool, }, IncomingRequest(Arc), OutgoingRequest(Arc), // ChannelInvite(Arc), Channel { channel: Arc, depth: usize, has_children: bool, }, ChannelNotes { channel_id: ChannelId, }, ChannelChat { channel_id: ChannelId, }, ChannelEditor { depth: usize, }, Contact { contact: Arc, calling: bool, }, ContactPlaceholder, } impl CollabPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { cx.build_view(|cx| { let view = cx.view().clone(); let filter_editor = cx.build_view(|cx| { let mut editor = Editor::single_line(cx); editor.set_placeholder_text("Filter...", cx); editor }); cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| { if let editor::EditorEvent::BufferEdited = event { let query = this.filter_editor.read(cx).text(cx); if !query.is_empty() { this.selection.take(); } this.update_entries(true, cx); if !query.is_empty() { this.selection = this .entries .iter() .position(|entry| !matches!(entry, ListEntry::Header(_))); } } else if let editor::EditorEvent::Blurred = event { let query = this.filter_editor.read(cx).text(cx); if query.is_empty() { this.selection.take(); this.update_entries(true, cx); } } }) .detach(); let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx)); cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| { if let editor::EditorEvent::Blurred = event { if let Some(state) = &this.channel_editing_state { if state.pending_name().is_some() { return; } } this.take_editing_state(cx); this.update_entries(false, cx); cx.notify(); } }) .detach(); let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { view.update(cx, |view, cx| view.render_list_entry(ix, cx)) }); let mut this = Self { width: None, focus_handle: cx.focus_handle(), channel_clipboard: None, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: None, list_state, channel_name_editor, filter_editor, entries: Vec::default(), channel_editing_state: None, selection: None, channel_store: ChannelStore::global(cx), user_store: workspace.user_store().clone(), project: workspace.project().clone(), subscriptions: Vec::default(), match_candidates: Vec::default(), collapsed_sections: vec![Section::Offline], collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), // context_menu_on_selected: true, drag_target_channel: ChannelDragTarget::None, }; this.update_entries(false, cx); // Update the dock position when the setting changes. let mut old_dock_position = this.position(cx); this.subscriptions.push(cx.observe_global::( move |this: &mut Self, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; cx.emit(PanelEvent::ChangePosition); } cx.notify(); }, )); let active_call = ActiveCall::global(cx); this.subscriptions .push(cx.observe(&this.user_store, |this, _, cx| { this.update_entries(true, cx) })); this.subscriptions .push(cx.observe(&this.channel_store, |this, _, cx| { this.update_entries(true, cx) })); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); this.subscriptions .push(cx.observe_flag::(move |_, this, cx| { this.update_entries(true, cx) })); this.subscriptions.push(cx.subscribe( &this.channel_store, |this, _channel_store, e, cx| match e { ChannelEvent::ChannelCreated(channel_id) | ChannelEvent::ChannelRenamed(channel_id) => { if this.take_editing_state(cx) { this.update_entries(false, cx); this.selection = this.entries.iter().position(|entry| { if let ListEntry::Channel { channel, .. } = entry { channel.id == *channel_id } else { false } }); } } }, )); this }) } fn contacts(&self, cx: &AppContext) -> Option>> { Some(self.user_store.read(cx).contacts().to_owned()) } pub async fn load( workspace: WeakView, mut cx: AsyncWindowContext, ) -> anyhow::Result> { let serialized_panel = cx .background_executor() .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) }) .await .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store")) .log_err() .flatten() .map(|panel| serde_json::from_str::(&panel)) .transpose() .log_err() .flatten(); workspace.update(&mut cx, |workspace, cx| { let panel = CollabPanel::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; panel.collapsed_channels = serialized_panel .collapsed_channels .unwrap_or_else(|| Vec::new()); cx.notify(); }); } panel }) } fn serialize(&mut self, cx: &mut ViewContext) { let width = self.width; let collapsed_channels = self.collapsed_channels.clone(); self.pending_serialization = cx.background_executor().spawn( async move { KEY_VALUE_STORE .write_kvp( COLLABORATION_PANEL_KEY.into(), serde_json::to_string(&SerializedCollabPanel { width, collapsed_channels: Some(collapsed_channels), })?, ) .await?; anyhow::Ok(()) } .log_err(), ); } fn scroll_to_item(&mut self, ix: usize) { self.list_state.scroll_to_reveal_item(ix) } fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); let user_store = self.user_store.read(cx); let query = self.filter_editor.read(cx).text(cx); let executor = cx.background_executor().clone(); let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); let old_entries = mem::take(&mut self.entries); let mut scroll_to_top = false; if let Some(room) = ActiveCall::global(cx).read(cx).room() { self.entries.push(ListEntry::Header(Section::ActiveCall)); if !old_entries .iter() .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) { scroll_to_top = true; } if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); if let Some(channel_id) = room.channel_id() { self.entries.push(ListEntry::ChannelNotes { channel_id }); self.entries.push(ListEntry::ChannelChat { channel_id }) } // Populate the active user. if let Some(user) = user_store.current_user() { self.match_candidates.clear(); self.match_candidates.push(StringMatchCandidate { id: 0, string: user.github_login.clone(), char_bag: user.github_login.chars().collect(), }); let matches = executor.block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), executor.clone(), )); if !matches.is_empty() { let user_id = user.id; self.entries.push(ListEntry::CallParticipant { user, peer_id: None, is_pending: false, }); let mut projects = room.local_participant().projects.iter().peekable(); while let Some(project) = projects.next() { self.entries.push(ListEntry::ParticipantProject { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: user_id, is_last: projects.peek().is_none() && !room.is_screen_sharing(), }); } if room.is_screen_sharing() { self.entries.push(ListEntry::ParticipantScreen { peer_id: None, is_last: true, }); } } } // Populate remote participants. self.match_candidates.clear(); self.match_candidates .extend(room.remote_participants().iter().map(|(_, participant)| { StringMatchCandidate { id: participant.user.id as usize, string: participant.user.github_login.clone(), char_bag: participant.user.github_login.chars().collect(), } })); let matches = executor.block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), executor.clone(), )); for mat in matches { let user_id = mat.candidate_id as u64; let participant = &room.remote_participants()[&user_id]; self.entries.push(ListEntry::CallParticipant { user: participant.user.clone(), peer_id: Some(participant.peer_id), is_pending: false, }); let mut projects = participant.projects.iter().peekable(); while let Some(project) = projects.next() { self.entries.push(ListEntry::ParticipantProject { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: participant.user.id, is_last: projects.peek().is_none() && participant.video_tracks.is_empty(), }); } if !participant.video_tracks.is_empty() { self.entries.push(ListEntry::ParticipantScreen { peer_id: Some(participant.peer_id), is_last: true, }); } } // Populate pending participants. self.match_candidates.clear(); self.match_candidates .extend(room.pending_participants().iter().enumerate().map( |(id, participant)| StringMatchCandidate { id, string: participant.github_login.clone(), char_bag: participant.github_login.chars().collect(), }, )); let matches = executor.block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), executor.clone(), )); self.entries .extend(matches.iter().map(|mat| ListEntry::CallParticipant { user: room.pending_participants()[mat.candidate_id].clone(), peer_id: None, is_pending: true, })); } } let mut request_entries = Vec::new(); if cx.has_flag::() { self.entries.push(ListEntry::Header(Section::Channels)); if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { self.match_candidates.clear(); self.match_candidates .extend(channel_store.ordered_channels().enumerate().map( |(ix, (_, channel))| StringMatchCandidate { id: ix, string: channel.name.clone().into(), char_bag: channel.name.chars().collect(), }, )); let matches = executor.block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), executor.clone(), )); if let Some(state) = &self.channel_editing_state { if matches!(state, ChannelEditingState::Create { location: None, .. }) { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } } let mut collapse_depth = None; for mat in matches { let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); let depth = channel.parent_path.len(); if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { collapse_depth = Some(depth); } else if let Some(collapsed_depth) = collapse_depth { if depth > collapsed_depth { continue; } if self.is_channel_collapsed(channel.id) { collapse_depth = Some(depth); } else { collapse_depth = None; } } let has_children = channel_store .channel_at_index(mat.candidate_id + 1) .map_or(false, |next_channel| { next_channel.parent_path.ends_with(&[channel.id]) }); match &self.channel_editing_state { Some(ChannelEditingState::Create { location: parent_id, .. }) if *parent_id == Some(channel.id) => { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, has_children: false, }); self.entries .push(ListEntry::ChannelEditor { depth: depth + 1 }); } Some(ChannelEditingState::Rename { location: parent_id, .. }) if parent_id == &channel.id => { self.entries.push(ListEntry::ChannelEditor { depth }); } _ => { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, has_children, }); } } } } // let channel_invites = channel_store.channel_invitations(); // if !channel_invites.is_empty() { // self.match_candidates.clear(); // self.match_candidates // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { // StringMatchCandidate { // id: ix, // string: channel.name.clone(), // char_bag: channel.name.chars().collect(), // } // })); // let matches = executor.block(match_strings( // &self.match_candidates, // &query, // true, // usize::MAX, // &Default::default(), // executor.clone(), // )); // request_entries.extend(matches.iter().map(|mat| { // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) // })); // if !request_entries.is_empty() { // self.entries // .push(ListEntry::Header(Section::ChannelInvites)); // if !self.collapsed_sections.contains(&Section::ChannelInvites) { // self.entries.append(&mut request_entries); // } // } // } } self.entries.push(ListEntry::Header(Section::Contacts)); request_entries.clear(); let incoming = user_store.incoming_contact_requests(); if !incoming.is_empty() { self.match_candidates.clear(); self.match_candidates .extend( incoming .iter() .enumerate() .map(|(ix, user)| StringMatchCandidate { id: ix, string: user.github_login.clone(), char_bag: user.github_login.chars().collect(), }), ); let matches = executor.block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), executor.clone(), )); request_entries.extend( matches .iter() .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), ); } let outgoing = user_store.outgoing_contact_requests(); if !outgoing.is_empty() { self.match_candidates.clear(); self.match_candidates .extend( outgoing .iter() .enumerate() .map(|(ix, user)| StringMatchCandidate { id: ix, string: user.github_login.clone(), char_bag: user.github_login.chars().collect(), }), ); let matches = executor.block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), executor.clone(), )); request_entries.extend( matches .iter() .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), ); } if !request_entries.is_empty() { self.entries .push(ListEntry::Header(Section::ContactRequests)); if !self.collapsed_sections.contains(&Section::ContactRequests) { self.entries.append(&mut request_entries); } } let contacts = user_store.contacts(); if !contacts.is_empty() { self.match_candidates.clear(); self.match_candidates .extend( contacts .iter() .enumerate() .map(|(ix, contact)| StringMatchCandidate { id: ix, string: contact.user.github_login.clone(), char_bag: contact.user.github_login.chars().collect(), }), ); let matches = executor.block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), executor.clone(), )); let (online_contacts, offline_contacts) = matches .iter() .partition::, _>(|mat| contacts[mat.candidate_id].online); for (matches, section) in [ (online_contacts, Section::Online), (offline_contacts, Section::Offline), ] { if !matches.is_empty() { self.entries.push(ListEntry::Header(section)); if !self.collapsed_sections.contains(§ion) { let active_call = &ActiveCall::global(cx).read(cx); for mat in matches { let contact = &contacts[mat.candidate_id]; self.entries.push(ListEntry::Contact { contact: contact.clone(), calling: active_call.pending_invites().contains(&contact.user.id), }); } } } } } if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { self.entries.push(ListEntry::ContactPlaceholder); } if select_same_item { if let Some(prev_selected_entry) = prev_selected_entry { self.selection.take(); for (ix, entry) in self.entries.iter().enumerate() { if *entry == prev_selected_entry { self.selection = Some(ix); break; } } } } else { self.selection = self.selection.and_then(|prev_selection| { if self.entries.is_empty() { None } else { Some(prev_selection.min(self.entries.len() - 1)) } }); } let old_scroll_top = self.list_state.logical_scroll_top(); self.list_state.reset(self.entries.len()); if scroll_to_top { self.list_state.scroll_to(ListOffset::default()); } else { // Attempt to maintain the same scroll position. if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { let new_scroll_top = self .entries .iter() .position(|entry| entry == old_top_entry) .map(|item_ix| ListOffset { item_ix, offset_in_item: old_scroll_top.offset_in_item, }) .or_else(|| { let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; let item_ix = self .entries .iter() .position(|entry| entry == entry_after_old_top)?; Some(ListOffset { item_ix, offset_in_item: Pixels::ZERO, }) }) .or_else(|| { let entry_before_old_top = old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; let item_ix = self .entries .iter() .position(|entry| entry == entry_before_old_top)?; Some(ListOffset { item_ix, offset_in_item: Pixels::ZERO, }) }); self.list_state .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); } } cx.notify(); } fn render_call_participant( &self, user: &Arc, peer_id: Option, is_pending: bool, cx: &mut ViewContext, ) -> impl IntoElement { let is_current_user = self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); let tooltip = format!("Follow {}", user.github_login); ListItem::new(SharedString::from(user.github_login.clone())) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) .end_slot(if is_pending { Label::new("Calling").color(Color::Muted).into_any_element() } else if is_current_user { IconButton::new("leave-call", Icon::Exit) .style(ButtonStyle::Subtle) .on_click(cx.listener(move |this, _, cx| { Self::leave_call(cx); })) .tooltip(|cx| Tooltip::text("Leave Call", cx)) .into_any_element() } else { div().into_any_element() }) .when_some(peer_id, |this, peer_id| { this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) .on_click(cx.listener(move |this, _, cx| { this.workspace .update(cx, |workspace, cx| workspace.follow(peer_id, cx)); })) }) } fn render_participant_project( &self, project_id: u64, worktree_root_names: &[String], host_user_id: u64, // is_current: bool, is_last: bool, // is_selected: bool, // theme: &theme::Theme, cx: &mut ViewContext, ) -> impl IntoElement { let project_name: SharedString = if worktree_root_names.is_empty() { "untitled".to_string() } else { worktree_root_names.join(", ") } .into(); let theme = cx.theme(); ListItem::new(project_id as usize) .on_click(cx.listener(move |this, _, cx| { this.workspace.update(cx, |workspace, cx| { let app_state = workspace.app_state().clone(); workspace::join_remote_project(project_id, host_user_id, app_state, cx) .detach_and_log_err(cx); }); })) .start_slot( h_stack() .gap_1() .child(render_tree_branch(is_last, cx)) .child(IconButton::new(0, Icon::Folder)), ) .child(Label::new(project_name.clone())) .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) // enum JoinProject {} // enum JoinProjectTooltip {} // let collab_theme = &theme.collab_panel; // let host_avatar_width = collab_theme // .contact_avatar // .width // .or(collab_theme.contact_avatar.height) // .unwrap_or(0.); // let tree_branch = collab_theme.tree_branch; // let content = // MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); // let row = if is_current { // collab_theme // .project_row // .in_state(true) // .style_for(&mut Default::default()) // } else { // collab_theme // .project_row // .in_state(is_selected) // .style_for(mouse_state) // }; // Flex::row() // .with_child(render_tree_branch( // tree_branch, // &row.name.text, // is_last, // vec2f(host_avatar_width, collab_theme.row_height), // cx.font_cache(), // )) // .with_child( // Svg::new("icons/file_icons/folder.svg") // .with_color(collab_theme.channel_hash.color) // .constrained() // .with_width(collab_theme.channel_hash.width) // .aligned() // .left(), // ) // .with_child( // Label::new(project_name.clone(), row.name.text.clone()) // .aligned() // .left() // .contained() // .with_style(row.name.container) // .flex(1., false), // ) // .constrained() // .with_height(collab_theme.row_height) // .contained() // .with_style(row.container) // }); // if is_current { // return content.into_any(); // } // content // .with_cursor_style(CursorStyle::PointingHand) // .on_click(MouseButton::Left, move |_, this, cx| { // if let Some(workspace) = this.workspace.upgrade(cx) { // let app_state = workspace.read(cx).app_state().clone(); // workspace::join_remote_project(project_id, host_user_id, app_state, cx) // .detach_and_log_err(cx); // } // }) // .with_tooltip::( // project_id as usize, // format!("Open {}", project_name), // None, // theme.tooltip.clone(), // cx, // ) // .into_any() } fn render_participant_screen( &self, peer_id: Option, is_last: bool, cx: &mut ViewContext, ) -> impl IntoElement { let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); ListItem::new(("screen", id)) .start_slot( h_stack() .gap_1() .child(render_tree_branch(is_last, cx)) .child(IconButton::new(0, Icon::Screen)), ) .child(Label::new("Screen")) .when_some(peer_id, |this, _| { this.on_click(cx.listener(move |this, _, cx| { this.workspace.update(cx, |workspace, cx| { workspace.open_shared_screen(peer_id.unwrap(), cx) }); })) .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx)) }) } fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { if let Some(_) = self.channel_editing_state.take() { self.channel_name_editor.update(cx, |editor, cx| { editor.set_text("", cx); }); true } else { false } } // fn render_contact_placeholder( // &self, // theme: &theme::CollabPanel, // is_selected: bool, // cx: &mut ViewContext, // ) -> AnyElement { // enum AddContacts {} // MouseEventHandler::new::(0, cx, |state, _| { // let style = theme.list_empty_state.style_for(is_selected, state); // Flex::row() // .with_child( // Svg::new("icons/plus.svg") // .with_color(theme.list_empty_icon.color) // .constrained() // .with_width(theme.list_empty_icon.width) // .aligned() // .left(), // ) // .with_child( // Label::new("Add a contact", style.text.clone()) // .contained() // .with_style(theme.list_empty_label_container), // ) // .align_children_center() // .contained() // .with_style(style.container) // .into_any() // }) // .on_click(MouseButton::Left, |_, this, cx| { // this.toggle_contact_finder(cx); // }) // .into_any() // } fn render_channel_notes( &self, channel_id: ChannelId, cx: &mut ViewContext, ) -> impl IntoElement { ListItem::new("channel-notes") .on_click(cx.listener(move |this, _, cx| { this.open_channel_notes(channel_id, cx); })) .start_slot( h_stack() .gap_1() .child(render_tree_branch(false, cx)) .child(IconButton::new(0, Icon::File)), ) .child(div().h_7().w_full().child(Label::new("notes"))) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) } fn render_channel_chat( &self, channel_id: ChannelId, cx: &mut ViewContext, ) -> impl IntoElement { ListItem::new("channel-chat") .on_click(cx.listener(move |this, _, cx| { this.join_channel_chat(channel_id, cx); })) .start_slot( h_stack() .gap_1() .child(render_tree_branch(false, cx)) .child(IconButton::new(0, Icon::MessageBubbles)), ) .child(Label::new("chat")) .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } // fn render_channel_invite( // channel: Arc, // channel_store: ModelHandle, // theme: &theme::CollabPanel, // is_selected: bool, // cx: &mut ViewContext, // ) -> AnyElement { // enum Decline {} // enum Accept {} // let channel_id = channel.id; // let is_invite_pending = channel_store // .read(cx) // .has_pending_channel_invite_response(&channel); // let button_spacing = theme.contact_button_spacing; // Flex::row() // .with_child( // Svg::new("icons/hash.svg") // .with_color(theme.channel_hash.color) // .constrained() // .with_width(theme.channel_hash.width) // .aligned() // .left(), // ) // .with_child( // Label::new(channel.name.clone(), theme.contact_username.text.clone()) // .contained() // .with_style(theme.contact_username.container) // .aligned() // .left() // .flex(1., true), // ) // .with_child( // MouseEventHandler::new::(channel.id as usize, cx, |mouse_state, _| { // let button_style = if is_invite_pending { // &theme.disabled_button // } else { // theme.contact_button.style_for(mouse_state) // }; // render_icon_button(button_style, "icons/x.svg").aligned() // }) // .with_cursor_style(CursorStyle::PointingHand) // .on_click(MouseButton::Left, move |_, this, cx| { // this.respond_to_channel_invite(channel_id, false, cx); // }) // .contained() // .with_margin_right(button_spacing), // ) // .with_child( // MouseEventHandler::new::(channel.id as usize, cx, |mouse_state, _| { // let button_style = if is_invite_pending { // &theme.disabled_button // } else { // theme.contact_button.style_for(mouse_state) // }; // render_icon_button(button_style, "icons/check.svg") // .aligned() // .flex_float() // }) // .with_cursor_style(CursorStyle::PointingHand) // .on_click(MouseButton::Left, move |_, this, cx| { // this.respond_to_channel_invite(channel_id, true, cx); // }), // ) // .constrained() // .with_height(theme.row_height) // .contained() // .with_style( // *theme // .contact_row // .in_state(is_selected) // .style_for(&mut Default::default()), // ) // .with_padding_left( // theme.contact_row.default_style().padding.left + theme.channel_indent, // ) // .into_any() // } fn has_subchannels(&self, ix: usize) -> bool { self.entries.get(ix).map_or(false, |entry| { if let ListEntry::Channel { has_children, .. } = entry { *has_children } else { false } }) } fn deploy_channel_context_menu( &mut self, position: Point, channel_id: ChannelId, ix: usize, cx: &mut ViewContext, ) { let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { self.channel_store .read(cx) .channel_for_id(clipboard.channel_id) .map(|channel| channel.name.clone()) }); let this = cx.view().clone(); let context_menu = ContextMenu::build(cx, |mut context_menu, cx| { if self.has_subchannels(ix) { let expand_action_name = if self.is_channel_collapsed(channel_id) { "Expand Subchannels" } else { "Collapse Subchannels" }; context_menu = context_menu.entry( expand_action_name, cx.handler_for(&this, move |this, cx| { this.toggle_channel_collapsed(channel_id, cx) }), ); } context_menu = context_menu .entry( "Open Notes", cx.handler_for(&this, move |this, cx| { this.open_channel_notes(channel_id, cx) }), ) .entry( "Open Chat", cx.handler_for(&this, move |this, cx| { this.join_channel_chat(channel_id, cx) }), ) .entry( "Copy Channel Link", cx.handler_for(&this, move |this, cx| { this.copy_channel_link(channel_id, cx) }), ); if self.channel_store.read(cx).is_channel_admin(channel_id) { context_menu = context_menu .separator() .entry( "New Subchannel", cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)), ) .entry( "Rename", cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)), ) .entry( "Move this channel", cx.handler_for(&this, move |this, cx| { this.start_move_channel(channel_id, cx) }), ); if let Some(channel_name) = clipboard_channel_name { context_menu = context_menu.separator().entry( format!("Move '#{}' here", channel_name), cx.handler_for(&this, move |this, cx| { this.move_channel_on_clipboard(channel_id, cx) }), ); } context_menu = context_menu .separator() .entry( "Invite Members", cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)), ) .entry( "Manage Members", cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)), ) .entry( "Delete", cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)), ); } context_menu }); cx.focus_view(&context_menu); let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { if this.context_menu.as_ref().is_some_and(|context_menu| { context_menu.0.focus_handle(cx).contains_focused(cx) }) { cx.focus_self(); } this.context_menu.take(); cx.notify(); }); self.context_menu = Some((context_menu, position, subscription)); cx.notify(); } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { if self.take_editing_state(cx) { cx.focus_view(&self.filter_editor); } else { self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { editor.set_text("", cx); } }); } self.update_entries(false, cx); } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { let ix = self.selection.map_or(0, |ix| ix + 1); if ix < self.entries.len() { self.selection = Some(ix); } if let Some(ix) = self.selection { self.scroll_to_item(ix) } cx.notify(); } fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { let ix = self.selection.take().unwrap_or(0); if ix > 0 { self.selection = Some(ix - 1); } if let Some(ix) = self.selection { self.scroll_to_item(ix) } cx.notify(); } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if self.confirm_channel_edit(cx) { return; } if let Some(selection) = self.selection { if let Some(entry) = self.entries.get(selection) { match entry { ListEntry::Header(section) => match section { Section::ActiveCall => Self::leave_call(cx), Section::Channels => self.new_root_channel(cx), Section::Contacts => self.toggle_contact_finder(cx), Section::ContactRequests | Section::Online | Section::Offline | Section::ChannelInvites => { self.toggle_section_expanded(*section, cx); } }, ListEntry::Contact { contact, calling } => { if contact.online && !contact.busy && !calling { self.call(contact.user.id, cx); } } // ListEntry::ParticipantProject { // project_id, // host_user_id, // .. // } => { // if let Some(workspace) = self.workspace.upgrade(cx) { // let app_state = workspace.read(cx).app_state().clone(); // workspace::join_remote_project( // *project_id, // *host_user_id, // app_state, // cx, // ) // .detach_and_log_err(cx); // } // } // ListEntry::ParticipantScreen { peer_id, .. } => { // let Some(peer_id) = peer_id else { // return; // }; // if let Some(workspace) = self.workspace.upgrade(cx) { // workspace.update(cx, |workspace, cx| { // workspace.open_shared_screen(*peer_id, cx) // }); // } // } ListEntry::Channel { channel, .. } => { let is_active = maybe!({ let call_channel = ActiveCall::global(cx) .read(cx) .room()? .read(cx) .channel_id()?; Some(call_channel == channel.id) }) .unwrap_or(false); if is_active { self.open_channel_notes(channel.id, cx) } else { self.join_channel(channel.id, cx) } } ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), _ => {} } } } } fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext) { if self.channel_editing_state.is_some() { self.channel_name_editor.update(cx, |editor, cx| { editor.insert(" ", cx); }); } } fn confirm_channel_edit(&mut self, cx: &mut ViewContext) -> bool { if let Some(editing_state) = &mut self.channel_editing_state { match editing_state { ChannelEditingState::Create { location, pending_name, .. } => { if pending_name.is_some() { return false; } let channel_name = self.channel_name_editor.read(cx).text(cx); *pending_name = Some(channel_name.clone()); self.channel_store .update(cx, |channel_store, cx| { channel_store.create_channel(&channel_name, *location, cx) }) .detach(); cx.notify(); } ChannelEditingState::Rename { location, pending_name, } => { if pending_name.is_some() { return false; } let channel_name = self.channel_name_editor.read(cx).text(cx); *pending_name = Some(channel_name.clone()); self.channel_store .update(cx, |channel_store, cx| { channel_store.rename(*location, &channel_name, cx) }) .detach(); cx.notify(); } } cx.focus_self(); true } else { false } } fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { self.collapsed_sections.remove(ix); } else { self.collapsed_sections.push(section); } self.update_entries(false, cx); } fn collapse_selected_channel( &mut self, _: &CollapseSelectedChannel, cx: &mut ViewContext, ) { let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { return; }; if self.is_channel_collapsed(channel_id) { return; } self.toggle_channel_collapsed(channel_id, cx); } fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { let Some(id) = self.selected_channel().map(|channel| channel.id) else { return; }; if !self.is_channel_collapsed(id) { return; } self.toggle_channel_collapsed(id, cx) } // fn toggle_channel_collapsed_action( // &mut self, // action: &ToggleCollapse, // cx: &mut ViewContext, // ) { // self.toggle_channel_collapsed(action.location, cx); // } fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { self.collapsed_channels.remove(ix); } Err(ix) => { self.collapsed_channels.insert(ix, channel_id); } }; self.serialize(cx); self.update_entries(true, cx); cx.notify(); cx.focus_self(); } fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { self.collapsed_channels.binary_search(&channel_id).is_ok() } fn leave_call(cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |call, cx| call.hang_up(cx)) .detach_and_log_err(cx); } fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade() { workspace.update(cx, |workspace, cx| { workspace.toggle_modal(cx, |cx| { let mut finder = ContactFinder::new(self.user_store.clone(), cx); finder.set_query(self.filter_editor.read(cx).text(cx), cx); finder }); }); } } fn new_root_channel(&mut self, cx: &mut ViewContext) { self.channel_editing_state = Some(ChannelEditingState::Create { location: None, pending_name: None, }); self.update_entries(false, cx); self.select_channel_editor(); cx.focus_view(&self.channel_name_editor); cx.notify(); } fn select_channel_editor(&mut self) { self.selection = self.entries.iter().position(|entry| match entry { ListEntry::ChannelEditor { .. } => true, _ => false, }); } fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { self.collapsed_channels .retain(|channel| *channel != channel_id); self.channel_editing_state = Some(ChannelEditingState::Create { location: Some(channel_id), pending_name: None, }); self.update_entries(false, cx); self.select_channel_editor(); cx.focus_view(&self.channel_name_editor); cx.notify(); } fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx); } fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx); } fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext) { if let Some(channel) = self.selected_channel() { self.remove_channel(channel.id, cx) } } fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { if let Some(channel) = self.selected_channel() { self.rename_channel(channel.id, cx); } } fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); if !channel_store.is_channel_admin(channel_id) { return; } if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() { self.channel_editing_state = Some(ChannelEditingState::Rename { location: channel_id, pending_name: None, }); self.channel_name_editor.update(cx, |editor, cx| { editor.set_text(channel.name.clone(), cx); editor.select_all(&Default::default(), cx); }); cx.focus_view(&self.channel_name_editor); self.update_entries(false, cx); self.select_channel_editor(); } } fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { self.channel_clipboard = Some(ChannelMoveClipboard { channel_id }); } fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { if let Some(channel) = self.selected_channel() { self.channel_clipboard = Some(ChannelMoveClipboard { channel_id: channel.id, }) } } fn move_channel_on_clipboard( &mut self, to_channel_id: ChannelId, cx: &mut ViewContext, ) { if let Some(clipboard) = self.channel_clipboard.take() { self.channel_store.update(cx, |channel_store, cx| { channel_store .move_channel(clipboard.channel_id, Some(to_channel_id), cx) .detach_and_log_err(cx) }) } } fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade() { ChannelView::open(channel_id, workspace, cx).detach(); } } fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { let Some(channel) = self.selected_channel() else { return; }; let Some(bounds) = self .selection .and_then(|ix| self.list_state.bounds_for_item(ix)) else { return; }; self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx); cx.stop_propagation(); } fn selected_channel(&self) -> Option<&Arc> { self.selection .and_then(|ix| self.entries.get(ix)) .and_then(|entry| match entry { ListEntry::Channel { channel, .. } => Some(channel), _ => None, }) } fn show_channel_modal( &mut self, channel_id: ChannelId, mode: channel_modal::Mode, cx: &mut ViewContext, ) { let workspace = self.workspace.clone(); let user_store = self.user_store.clone(); let channel_store = self.channel_store.clone(); let members = self.channel_store.update(cx, |channel_store, cx| { channel_store.get_channel_member_details(channel_id, cx) }); cx.spawn(|_, mut cx| async move { let members = members.await?; workspace.update(&mut cx, |workspace, cx| { workspace.toggle_modal(cx, |cx| { ChannelModal::new( user_store.clone(), channel_store.clone(), channel_id, mode, members, cx, ) }); }) }) .detach(); } // fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { // self.remove_channel(action.channel_id, cx) // } fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { let channel_store = self.channel_store.clone(); if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { let prompt_message = format!( "Are you sure you want to remove the channel \"{}\"?", channel.name ); let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); let window = cx.window(); cx.spawn(|this, mut cx| async move { if answer.await? == 0 { channel_store .update(&mut cx, |channels, _| channels.remove_channel(channel_id))? .await .notify_async_err(&mut cx); this.update(&mut cx, |_, cx| cx.focus_self()).ok(); } anyhow::Ok(()) }) .detach(); } } // // Should move to the filter editor if clicking on it // // Should move selection to the channel editor if activating it fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { let user_store = self.user_store.clone(); let prompt_message = format!( "Are you sure you want to remove \"{}\" from your contacts?", github_login ); let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); let window = cx.window(); cx.spawn(|_, mut cx| async move { if answer.await? == 0 { user_store .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))? .await .notify_async_err(&mut cx); } anyhow::Ok(()) }) .detach_and_log_err(cx); } fn respond_to_contact_request( &mut self, user_id: u64, accept: bool, cx: &mut ViewContext, ) { self.user_store .update(cx, |store, cx| { store.respond_to_contact_request(user_id, accept, cx) }) .detach_and_log_err(cx); } // fn respond_to_channel_invite( // &mut self, // channel_id: u64, // accept: bool, // cx: &mut ViewContext, // ) { // self.channel_store // .update(cx, |store, cx| { // store.respond_to_channel_invite(channel_id, accept, cx) // }) // .detach(); // } fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |call, cx| { call.invite(recipient_user_id, Some(self.project.clone()), cx) }) .detach_and_log_err(cx); } fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { let Some(workspace) = self.workspace.upgrade() else { return; }; let Some(handle) = cx.window_handle().downcast::() else { return; }; workspace::join_channel( channel_id, workspace.read(cx).app_state().clone(), Some(handle), cx, ) .detach_and_log_err(cx) } fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { let Some(workspace) = self.workspace.upgrade() else { return; }; cx.window_context().defer(move |cx| { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.focus_panel::(cx) { panel.update(cx, |panel, cx| { panel .select_channel(channel_id, None, cx) .detach_and_log_err(cx); }); } }); }); } fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); let Some(channel) = channel_store.channel_for_id(channel_id) else { return; }; let item = ClipboardItem::new(channel.link()); cx.write_to_clipboard(item) } fn render_signed_out(&mut self, cx: &mut ViewContext) -> Div { v_stack() .items_center() .child(v_stack().gap_6().p_4() .child( Label::new("Work with your team in realtime with collaborative editing, voice, shared notes and more.") ) .child(v_stack().gap_2() .child( Button::new("sign_in", "Sign in") .icon_color(Color::Muted) .icon(Icon::Github) .icon_position(IconPosition::Start) .style(ButtonStyle::Filled) .full_width() .on_click(cx.listener( |this, _, cx| { let client = this.client.clone(); cx.spawn(|_, mut cx| async move { client .authenticate_and_connect(true, &cx) .await .notify_async_err(&mut cx); }) .detach() }, ))) .child( div().flex().w_full().items_center().child( Label::new("Sign in to enable collaboration.") .color(Color::Muted) .size(LabelSize::Small) )), )) } fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { let entry = &self.entries[ix]; let is_selected = self.selection == Some(ix); match entry { ListEntry::Header(section) => { let is_collapsed = self.collapsed_sections.contains(section); self.render_header(*section, is_selected, is_collapsed, cx) .into_any_element() } ListEntry::Contact { contact, calling } => self .render_contact(contact, *calling, is_selected, cx) .into_any_element(), ListEntry::ContactPlaceholder => self .render_contact_placeholder(is_selected, cx) .into_any_element(), ListEntry::IncomingRequest(user) => self .render_contact_request(user, true, is_selected, cx) .into_any_element(), ListEntry::OutgoingRequest(user) => self .render_contact_request(user, false, is_selected, cx) .into_any_element(), ListEntry::Channel { channel, depth, has_children, } => self .render_channel(channel, *depth, *has_children, is_selected, ix, cx) .into_any_element(), ListEntry::ChannelEditor { depth } => { self.render_channel_editor(*depth, cx).into_any_element() } ListEntry::CallParticipant { user, peer_id, is_pending, } => self .render_call_participant(user, *peer_id, *is_pending, cx) .into_any_element(), ListEntry::ParticipantProject { project_id, worktree_root_names, host_user_id, is_last, } => self .render_participant_project( *project_id, &worktree_root_names, *host_user_id, *is_last, cx, ) .into_any_element(), ListEntry::ParticipantScreen { peer_id, is_last } => self .render_participant_screen(*peer_id, *is_last, cx) .into_any_element(), ListEntry::ChannelNotes { channel_id } => self .render_channel_notes(*channel_id, cx) .into_any_element(), ListEntry::ChannelChat { channel_id } => { self.render_channel_chat(*channel_id, cx).into_any_element() } } } fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { v_stack() .size_full() .child(list(self.list_state.clone()).full()) .child( v_stack().p_2().child( v_stack() .border_primary(cx) .border_t() .child(self.filter_editor.clone()), ), ) } fn render_header( &self, section: Section, is_selected: bool, is_collapsed: bool, cx: &ViewContext, ) -> impl IntoElement { let mut channel_link = None; let mut channel_tooltip_text = None; let mut channel_icon = None; // let mut is_dragged_over = false; let text = match section { Section::ActiveCall => { let channel_name = maybe!({ let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; channel_link = Some(channel.link()); (channel_icon, channel_tooltip_text) = match channel.visibility { proto::ChannelVisibility::Public => { (Some("icons/public.svg"), Some("Copy public channel link.")) } proto::ChannelVisibility::Members => { (Some("icons/hash.svg"), Some("Copy private channel link.")) } }; Some(channel.name.as_ref()) }); if let Some(name) = channel_name { SharedString::from(format!("{}", name)) } else { SharedString::from("Current Call") } } Section::ContactRequests => SharedString::from("Requests"), Section::Contacts => SharedString::from("Contacts"), Section::Channels => SharedString::from("Channels"), Section::ChannelInvites => SharedString::from("Invites"), Section::Online => SharedString::from("Online"), Section::Offline => SharedString::from("Offline"), }; let button = match section { Section::ActiveCall => channel_link.map(|channel_link| { let channel_link_copy = channel_link.clone(); IconButton::new("channel-link", Icon::Copy) .icon_size(IconSize::Small) .size(ButtonSize::None) .visible_on_hover("section-header") .on_click(move |_, cx| { let item = ClipboardItem::new(channel_link_copy.clone()); cx.write_to_clipboard(item) }) .tooltip(|cx| Tooltip::text("Copy channel link", cx)) .into_any_element() }), Section::Contacts => Some( div() .border_1() .border_color(gpui::red()) .child( IconButton::new("add-contact", Icon::Plus) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) .tooltip(|cx| Tooltip::text("Search for new contact", cx)), ) .into_any_element(), ), Section::Channels => Some( IconButton::new("add-channel", Icon::Plus) .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) .tooltip(|cx| Tooltip::text("Create a channel", cx)) .into_any_element(), ), _ => None, }; let can_collapse = match section { Section::ActiveCall | Section::Channels | Section::Contacts => false, Section::ChannelInvites | Section::ContactRequests | Section::Online | Section::Offline => true, }; h_stack() .w_full() .group("section-header") .child( ListHeader::new(text) .when(can_collapse, |header| { header.toggle(Some(!is_collapsed)).on_toggle(cx.listener( move |this, event, cx| { this.toggle_section_expanded(section, cx); }, )) }) .inset(true) .end_slot::(button) .selected(is_selected), ) .when(section == Section::Channels, |el| { el.drag_over::(|style| style.bg(cx.theme().colors().ghost_element_hover)) .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| { this.channel_store .update(cx, |channel_store, cx| { channel_store.move_channel(dragged_channel.id, None, cx) }) .detach_and_log_err(cx) })) }) } fn render_contact( &self, contact: &Contact, calling: bool, is_selected: bool, cx: &mut ViewContext, ) -> impl IntoElement { let online = contact.online; let busy = contact.busy || calling; let user_id = contact.user.id; let github_login = SharedString::from(contact.user.github_login.clone()); let mut item = ListItem::new(github_login.clone()) .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) .child( h_stack() .w_full() .justify_between() .child(Label::new(github_login.clone())) .when(calling, |el| { el.child(Label::new("Calling").color(Color::Muted)) }) .when(!calling, |el| { el.child( IconButton::new("remove_contact", Icon::Close) .icon_color(Color::Muted) .visible_on_hover("") .tooltip(|cx| Tooltip::text("Remove Contact", cx)) .on_click(cx.listener({ let github_login = github_login.clone(); move |this, _, cx| { this.remove_contact(user_id, &github_login, cx); } })), ) }), ) .start_slot( // todo!() handle contacts with no avatar Avatar::new(contact.user.avatar_uri.clone()) .availability_indicator(if online { Some(!busy) } else { None }), ) .when(online && !busy, |el| { el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) }); div() .id(github_login.clone()) .group("") .child(item) .tooltip(move |cx| { let text = if !online { format!(" {} is offline", &github_login) } else if busy { format!(" {} is on a call", &github_login) } else { let room = ActiveCall::global(cx).read(cx).room(); if room.is_some() { format!("Invite {} to join call", &github_login) } else { format!("Call {}", &github_login) } }; Tooltip::text(text, cx) }) } fn render_contact_request( &self, user: &Arc, is_incoming: bool, is_selected: bool, cx: &mut ViewContext, ) -> impl IntoElement { let github_login = SharedString::from(user.github_login.clone()); let user_id = user.id; let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user); let color = if is_contact_request_pending { Color::Muted } else { Color::Default }; let controls = if is_incoming { vec![ IconButton::new("remove_contact", Icon::Close) .on_click(cx.listener(move |this, _, cx| { this.respond_to_contact_request(user_id, false, cx); })) .icon_color(color) .tooltip(|cx| Tooltip::text("Decline invite", cx)), IconButton::new("remove_contact", Icon::Check) .on_click(cx.listener(move |this, _, cx| { this.respond_to_contact_request(user_id, true, cx); })) .icon_color(color) .tooltip(|cx| Tooltip::text("Accept invite", cx)), ] } else { let github_login = github_login.clone(); vec![IconButton::new("remove_contact", Icon::Close) .on_click(cx.listener(move |this, _, cx| { this.remove_contact(user_id, &github_login, cx); })) .icon_color(color) .tooltip(|cx| Tooltip::text("Cancel invite", cx))] }; ListItem::new(github_login.clone()) .child( h_stack() .w_full() .justify_between() .child(Label::new(github_login.clone())) .child(h_stack().children(controls)), ) .start_slot(Avatar::new(user.avatar_uri.clone())) } fn render_contact_placeholder( &self, is_selected: bool, cx: &mut ViewContext, ) -> impl IntoElement { ListItem::new("contact-placeholder") .child(IconElement::new(Icon::Plus)) .child(Label::new("Add a Contact")) .selected(is_selected) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) } fn render_channel( &self, channel: &Channel, depth: usize, has_children: bool, is_selected: bool, ix: usize, cx: &mut ViewContext, ) -> impl IntoElement { let channel_id = channel.id; let is_active = maybe!({ let call_channel = ActiveCall::global(cx) .read(cx) .room()? .read(cx) .channel_id()?; Some(call_channel == channel_id) }) .unwrap_or(false); let is_public = self .channel_store .read(cx) .channel_for_id(channel_id) .map(|channel| channel.visibility) == Some(proto::ChannelVisibility::Public); let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); let has_messages_notification = channel.unseen_message_id.is_some(); let has_notes_notification = channel.unseen_note_version.is_some(); const FACEPILE_LIMIT: usize = 3; let participants = self.channel_store.read(cx).channel_participants(channel_id); let face_pile = if !participants.is_empty() { let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); let user = &participants[0]; let result = FacePile { faces: participants .iter() .filter_map(|user| { Some(Avatar::new(user.avatar_uri.clone()).into_any_element()) }) .take(FACEPILE_LIMIT) .chain(if extra_count > 0 { // todo!() @nate - this label looks wrong. Some(Label::new(format!("+{}", extra_count)).into_any_element()) } else { None }) .collect::>(), }; Some(result) } else { None }; let width = self.width.unwrap_or(px(240.)); div() .id(channel_id as usize) .group("") .flex() .w_full() .on_drag(channel.clone(), move |channel, cx| { cx.build_view(|cx| DraggedChannelView { channel: channel.clone(), width, }) }) .drag_over::(|style| style.bg(cx.theme().colors().ghost_element_hover)) .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| { this.channel_store .update(cx, |channel_store, cx| { channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) }) .detach_and_log_err(cx) })) .child( ListItem::new(channel_id as usize) // Offset the indent depth by one to give us room to show the disclosure. .indent_level(depth + 1) .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle .selected(is_selected || is_active) .toggle(disclosed) .on_toggle( cx.listener(move |this, _, cx| { this.toggle_channel_collapsed(channel_id, cx) }), ) .on_click(cx.listener(move |this, _, cx| { if this.drag_target_channel == ChannelDragTarget::None { if is_active { this.open_channel_notes(channel_id, cx) } else { this.join_channel(channel_id, cx) } } })) .on_secondary_mouse_down(cx.listener( move |this, event: &MouseDownEvent, cx| { this.deploy_channel_context_menu(event.position, channel_id, ix, cx) }, )) .start_slot( IconElement::new(if is_public { Icon::Public } else { Icon::Hash }) .size(IconSize::Small) .color(Color::Muted), ) .child( h_stack() .id(channel_id as usize) .child(Label::new(channel.name.clone())) .children(face_pile.map(|face_pile| face_pile.render(cx))), ) .end_slot( h_stack() .child( IconButton::new("channel_chat", Icon::MessageBubbles) .icon_color(if has_messages_notification { Color::Default } else { Color::Muted }) .when(!has_messages_notification, |this| { this.visible_on_hover("") }) .on_click(cx.listener(move |this, _, cx| { this.join_channel_chat(channel_id, cx) })) .tooltip(|cx| Tooltip::text("Open channel chat", cx)), ) .child( IconButton::new("channel_notes", Icon::File) .icon_color(if has_notes_notification { Color::Default } else { Color::Muted }) .when(!has_notes_notification, |this| this.visible_on_hover("")) .on_click(cx.listener(move |this, _, cx| { this.open_channel_notes(channel_id, cx) })) .tooltip(|cx| Tooltip::text("Open channel notes", cx)), ), ), ) .tooltip(|cx| Tooltip::text("Join channel", cx)) // let channel_id = channel.id; // let collab_theme = &theme.collab_panel; // let is_public = self // .channel_store // .read(cx) // .channel_for_id(channel_id) // .map(|channel| channel.visibility) // == Some(proto::ChannelVisibility::Public); // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); // let disclosed = // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); // enum ChannelCall {} // enum ChannelNote {} // enum NotesTooltip {} // enum ChatTooltip {} // enum ChannelTooltip {} // let mut is_dragged_over = false; // if cx // .global::>() // .currently_dragged::(cx.window()) // .is_some() // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) // { // is_dragged_over = true; // } // let has_messages_notification = channel.unseen_message_id.is_some(); // MouseEventHandler::new::(ix, cx, |state, cx| { // let row_hovered = state.hovered(); // let mut select_state = |interactive: &Interactive| { // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { // interactive.clicked.as_ref().unwrap().clone() // } else if state.hovered() || other_selected { // interactive // .hovered // .as_ref() // .unwrap_or(&interactive.default) // .clone() // } else { // interactive.default.clone() // } // }; // Flex::::row() // .with_child( // Svg::new(if is_public { // "icons/public.svg" // } else { // "icons/hash.svg" // }) // .with_color(collab_theme.channel_hash.color) // .constrained() // .with_width(collab_theme.channel_hash.width) // .aligned() // .left(), // ) // .with_child({ // let style = collab_theme.channel_name.inactive_state(); // Flex::row() // .with_child( // Label::new(channel.name.clone(), style.text.clone()) // .contained() // .with_style(style.container) // .aligned() // .left() // .with_tooltip::( // ix, // "Join channel", // None, // theme.tooltip.clone(), // cx, // ), // ) // .with_children({ // let participants = // self.channel_store.read(cx).channel_participants(channel_id); // if !participants.is_empty() { // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); // let result = FacePile::new(collab_theme.face_overlap) // .with_children( // participants // .iter() // .filter_map(|user| { // Some( // Image::from_data(user.avatar.clone()?) // .with_style(collab_theme.channel_avatar), // ) // }) // .take(FACEPILE_LIMIT), // ) // .with_children((extra_count > 0).then(|| { // Label::new( // format!("+{}", extra_count), // collab_theme.extra_participant_label.text.clone(), // ) // .contained() // .with_style(collab_theme.extra_participant_label.container) // })); // Some(result) // } else { // None // } // }) // .with_spacing(8.) // .align_children_center() // .flex(1., true) // }) // .with_child( // MouseEventHandler::new::(ix, cx, move |mouse_state, _| { // let container_style = collab_theme // .disclosure // .button // .style_for(mouse_state) // .container; // if channel.unseen_message_id.is_some() { // Svg::new("icons/conversations.svg") // .with_color(collab_theme.channel_note_active_color) // .constrained() // .with_width(collab_theme.channel_hash.width) // .contained() // .with_style(container_style) // .with_uniform_padding(4.) // .into_any() // } else if row_hovered { // Svg::new("icons/conversations.svg") // .with_color(collab_theme.channel_hash.color) // .constrained() // .with_width(collab_theme.channel_hash.width) // .contained() // .with_style(container_style) // .with_uniform_padding(4.) // .into_any() // } else { // Empty::new().into_any() // } // }) // .on_click(MouseButton::Left, move |_, this, cx| { // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); // }) // .with_tooltip::( // ix, // "Open channel chat", // None, // theme.tooltip.clone(), // cx, // ) // .contained() // .with_margin_right(4.), // ) // .with_child( // MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { // let container_style = collab_theme // .disclosure // .button // .style_for(mouse_state) // .container; // if row_hovered || channel.unseen_note_version.is_some() { // Svg::new("icons/file.svg") // .with_color(if channel.unseen_note_version.is_some() { // collab_theme.channel_note_active_color // } else { // collab_theme.channel_hash.color // }) // .constrained() // .with_width(collab_theme.channel_hash.width) // .contained() // .with_style(container_style) // .with_uniform_padding(4.) // .with_margin_right(collab_theme.channel_hash.container.margin.left) // .with_tooltip::( // ix as usize, // "Open channel notes", // None, // theme.tooltip.clone(), // cx, // ) // .into_any() // } else if has_messages_notification { // Empty::new() // .constrained() // .with_width(collab_theme.channel_hash.width) // .contained() // .with_uniform_padding(4.) // .with_margin_right(collab_theme.channel_hash.container.margin.left) // .into_any() // } else { // Empty::new().into_any() // } // }) // .on_click(MouseButton::Left, move |_, this, cx| { // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); // }), // ) // .align_children_center() // .styleable_component() // .disclosable( // disclosed, // Box::new(ToggleCollapse { // location: channel.id.clone(), // }), // ) // .with_id(ix) // .with_style(collab_theme.disclosure.clone()) // .element() // .constrained() // .with_height(collab_theme.row_height) // .contained() // .with_style(select_state( // collab_theme // .channel_row // .in_state(is_selected || is_active || is_dragged_over), // )) // .with_padding_left( // collab_theme.channel_row.default_style().padding.left // + collab_theme.channel_indent * depth as f32, // ) // }) // .on_click(MouseButton::Left, move |_, this, cx| { // if this. // drag_target_channel == ChannelDragTarget::None { // if is_active { // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) // } else { // this.join_channel(channel_id, cx) // } // } // }) // .on_click(MouseButton::Right, { // let channel = channel.clone(); // move |e, this, cx| { // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); // } // }) // .on_up(MouseButton::Left, move |_, this, cx| { // if let Some((_, dragged_channel)) = cx // .global::>() // .currently_dragged::(cx.window()) // { // this.channel_store // .update(cx, |channel_store, cx| { // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) // }) // .detach_and_log_err(cx) // } // }) // .on_move({ // let channel = channel.clone(); // move |_, this, cx| { // if let Some((_, dragged_channel)) = cx // .global::>() // .currently_dragged::(cx.window()) // { // if channel.id != dragged_channel.id { // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); // } // cx.notify() // } // } // }) // .as_draggable::<_, Channel>( // channel.clone(), // move |_, channel, cx: &mut ViewContext| { // let theme = &theme::current(cx).collab_panel; // Flex::::row() // .with_child( // Svg::new("icons/hash.svg") // .with_color(theme.channel_hash.color) // .constrained() // .with_width(theme.channel_hash.width) // .aligned() // .left(), // ) // .with_child( // Label::new(channel.name.clone(), theme.channel_name.text.clone()) // .contained() // .with_style(theme.channel_name.container) // .aligned() // .left(), // ) // .align_children_center() // .contained() // .with_background_color( // theme // .container // .background_color // .unwrap_or(gpui::color::Color::transparent_black()), // ) // .contained() // .with_padding_left( // theme.channel_row.default_style().padding.left // + theme.channel_indent * depth as f32, // ) // .into_any() // }, // ) // .with_cursor_style(CursorStyle::PointingHand) // .into_any() } fn render_channel_editor(&self, depth: usize, cx: &mut ViewContext) -> impl IntoElement { let item = ListItem::new("channel-editor") .inset(false) .indent_level(depth) .start_slot( IconElement::new(Icon::Hash) .size(IconSize::Small) .color(Color::Muted), ); if let Some(pending_name) = self .channel_editing_state .as_ref() .and_then(|state| state.pending_name()) { item.child(Label::new(pending_name)) } else { item.child( div() .w_full() .py_1() // todo!() @nate this is a px off at the default font size. .child(self.channel_name_editor.clone()), ) } } } fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement { let rem_size = cx.rem_size(); let line_height = cx.text_style().line_height_in_pixels(rem_size); let width = rem_size * 1.5; let thickness = px(2.); let color = cx.theme().colors().text; canvas(move |bounds, cx| { let start_x = (bounds.left() + bounds.right() - thickness) / 2.; let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.; let right = bounds.right(); let top = bounds.top(); cx.paint_quad(fill( Bounds::from_corners( point(start_x, top), point( start_x + thickness, if is_last { start_y } else { bounds.bottom() }, ), ), color, )); cx.paint_quad(fill( Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)), color, )); }) .w(width) .h(line_height) } impl Render for CollabPanel { type Element = Focusable
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { v_stack() .key_context("CollabPanel") .on_action(cx.listener(CollabPanel::cancel)) .on_action(cx.listener(CollabPanel::select_next)) .on_action(cx.listener(CollabPanel::select_prev)) .on_action(cx.listener(CollabPanel::confirm)) .on_action(cx.listener(CollabPanel::insert_space)) // .on_action(cx.listener(CollabPanel::remove)) .on_action(cx.listener(CollabPanel::remove_selected_channel)) .on_action(cx.listener(CollabPanel::show_inline_context_menu)) // .on_action(cx.listener(CollabPanel::new_subchannel)) // .on_action(cx.listener(CollabPanel::invite_members)) // .on_action(cx.listener(CollabPanel::manage_members)) .on_action(cx.listener(CollabPanel::rename_selected_channel)) // .on_action(cx.listener(CollabPanel::rename_channel)) // .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action)) .on_action(cx.listener(CollabPanel::collapse_selected_channel)) .on_action(cx.listener(CollabPanel::expand_selected_channel)) // .on_action(cx.listener(CollabPanel::open_channel_notes)) // .on_action(cx.listener(CollabPanel::join_channel_chat)) // .on_action(cx.listener(CollabPanel::copy_channel_link)) .track_focus(&self.focus_handle) .size_full() .child(if self.user_store.read(cx).current_user().is_none() { self.render_signed_out(cx) } else { self.render_signed_in(cx) }) .children(self.context_menu.as_ref().map(|(menu, position, _)| { overlay() .position(*position) .anchor(gpui::AnchorCorner::TopLeft) .child(menu.clone()) })) } } // impl View for CollabPanel { // fn ui_name() -> &'static str { // "CollabPanel" // } // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { // if !self.has_focus { // self.has_focus = true; // if !self.context_menu.is_focused(cx) { // if let Some(editing_state) = &self.channel_editing_state { // if editing_state.pending_name().is_none() { // cx.focus(&self.channel_name_editor); // } else { // cx.focus(&self.filter_editor); // } // } else { // cx.focus(&self.filter_editor); // } // } // cx.emit(Event::Focus); // } // } // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { // self.has_focus = false; // } // fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { // let theme = &theme::current(cx).collab_panel; // if self.user_store.read(cx).current_user().is_none() { // enum LogInButton {} // return Flex::column() // .with_child( // MouseEventHandler::new::(0, cx, |state, _| { // let button = theme.log_in_button.style_for(state); // Label::new("Sign in to collaborate", button.text.clone()) // .aligned() // .left() // .contained() // .with_style(button.container) // }) // .on_click(MouseButton::Left, |_, this, cx| { // let client = this.client.clone(); // cx.spawn(|_, cx| async move { // client.authenticate_and_connect(true, &cx).await.log_err(); // }) // .detach(); // }) // .with_cursor_style(CursorStyle::PointingHand), // ) // .contained() // .with_style(theme.container) // .into_any(); // } // enum PanelFocus {} // MouseEventHandler::new::(0, cx, |_, cx| { // Stack::new() // .with_child( // Flex::column() // .with_child( // Flex::row().with_child( // ChildView::new(&self.filter_editor, cx) // .contained() // .with_style(theme.user_query_editor.container) // .flex(1.0, true), // ), // ) // .with_child(List::new(self.list_state.clone()).flex(1., true).into_any()) // .contained() // .with_style(theme.container) // .into_any(), // ) // .with_children( // (!self.context_menu_on_selected) // .then(|| ChildView::new(&self.context_menu, cx)), // ) // .into_any() // }) // .on_click(MouseButton::Left, |_, _, cx| cx.focus_self()) // .into_any_named("collab panel") // } // fn update_keymap_context( // &self, // keymap: &mut gpui::keymap_matcher::KeymapContext, // _: &AppContext, // ) { // Self::reset_to_default_keymap_context(keymap); // if self.channel_editing_state.is_some() { // keymap.add_identifier("editing"); // } else { // keymap.add_identifier("not_editing"); // } // } // } impl EventEmitter for CollabPanel {} impl Panel for CollabPanel { fn position(&self, cx: &gpui::WindowContext) -> DockPosition { CollaborationPanelSettings::get_global(cx).dock } fn position_is_valid(&self, position: DockPosition) -> bool { matches!(position, DockPosition::Left | DockPosition::Right) } fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { settings::update_settings_file::( self.fs.clone(), cx, move |settings| settings.dock = Some(position), ); } fn size(&self, cx: &gpui::WindowContext) -> f32 { self.width.map_or_else( || CollaborationPanelSettings::get_global(cx).default_width, |width| width.0, ) } fn set_size(&mut self, size: Option, cx: &mut ViewContext) { self.width = size.map(|s| px(s)); self.serialize(cx); cx.notify(); } fn icon(&self, cx: &gpui::WindowContext) -> Option { CollaborationPanelSettings::get_global(cx) .button .then(|| ui::Icon::Collab) } fn toggle_action(&self) -> Box { Box::new(ToggleFocus) } fn persistent_name() -> &'static str { "CollabPanel" } } impl FocusableView for CollabPanel { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { self.filter_editor.focus_handle(cx).clone() } } impl PartialEq for ListEntry { fn eq(&self, other: &Self) -> bool { match self { ListEntry::Header(section_1) => { if let ListEntry::Header(section_2) = other { return section_1 == section_2; } } ListEntry::CallParticipant { user: user_1, .. } => { if let ListEntry::CallParticipant { user: user_2, .. } = other { return user_1.id == user_2.id; } } ListEntry::ParticipantProject { project_id: project_id_1, .. } => { if let ListEntry::ParticipantProject { project_id: project_id_2, .. } = other { return project_id_1 == project_id_2; } } ListEntry::ParticipantScreen { peer_id: peer_id_1, .. } => { if let ListEntry::ParticipantScreen { peer_id: peer_id_2, .. } = other { return peer_id_1 == peer_id_2; } } ListEntry::Channel { channel: channel_1, .. } => { if let ListEntry::Channel { channel: channel_2, .. } = other { return channel_1.id == channel_2.id; } } ListEntry::ChannelNotes { channel_id } => { if let ListEntry::ChannelNotes { channel_id: other_id, } = other { return channel_id == other_id; } } ListEntry::ChannelChat { channel_id } => { if let ListEntry::ChannelChat { channel_id: other_id, } = other { return channel_id == other_id; } } // ListEntry::ChannelInvite(channel_1) => { // if let ListEntry::ChannelInvite(channel_2) = other { // return channel_1.id == channel_2.id; // } // } ListEntry::IncomingRequest(user_1) => { if let ListEntry::IncomingRequest(user_2) = other { return user_1.id == user_2.id; } } ListEntry::OutgoingRequest(user_1) => { if let ListEntry::OutgoingRequest(user_2) = other { return user_1.id == user_2.id; } } ListEntry::Contact { contact: contact_1, .. } => { if let ListEntry::Contact { contact: contact_2, .. } = other { return contact_1.user.id == contact_2.user.id; } } ListEntry::ChannelEditor { depth } => { if let ListEntry::ChannelEditor { depth: other_depth } = other { return depth == other_depth; } } ListEntry::ContactPlaceholder => { if let ListEntry::ContactPlaceholder = other { return true; } } } false } } // fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { // Svg::new(svg_path) // .with_color(style.color) // .constrained() // .with_width(style.icon_width) // .aligned() // .constrained() // .with_width(style.button_width) // .with_height(style.button_width) // .contained() // .with_style(style.container) // } struct DraggedChannelView { channel: Channel, width: Pixels, } impl Render for DraggedChannelView { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); h_stack() .font(ui_font) .bg(cx.theme().colors().background) .w(self.width) .p_1() .gap_1() .child( IconElement::new( if self.channel.visibility == proto::ChannelVisibility::Public { Icon::Public } else { Icon::Hash }, ) .size(IconSize::Small) .color(Color::Muted), ) .child(Label::new(self.channel.name.clone())) } }