From c2121c25c1c657d1eada1f4af0def3796553ca01 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 8 Sep 2023 17:06:39 -0700 Subject: [PATCH] Restructure collab panel to allow opening chat + notes w/ one click --- crates/channel/src/channel_chat.rs | 4 +- crates/channel/src/channel_store.rs | 6 + crates/collab_ui/src/chat_panel.rs | 52 +++--- crates/collab_ui/src/collab_panel.rs | 257 ++++++++++++++++++++------- crates/gpui/src/views.rs | 4 +- crates/gpui/src/views/select.rs | 26 +-- 6 files changed, 234 insertions(+), 115 deletions(-) diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 3916189363a040aba0b84d619c9bedbec8d72df8..f821e2ed8e55b41174e7e7ced3b6d48f89a3c80b 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -106,8 +106,8 @@ impl ChannelChat { })) } - pub fn name(&self) -> &str { - &self.channel.name + pub fn channel(&self) -> &Arc { + &self.channel } pub fn send_message( diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 48228041ecceaa18fe23aec902bd9566d877d158..bdfc75e8142aa928fa3e4f5510cb169b98b6f40f 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -129,6 +129,12 @@ impl ChannelStore { self.channel_paths.len() } + pub fn index_of_channel(&self, channel_id: ChannelId) -> Option { + self.channel_paths + .iter() + .position(|path| path.ends_with(&[channel_id])) + } + pub fn channels(&self) -> impl '_ + Iterator)> { self.channel_paths.iter().map(move |path| { let id = path.last().unwrap(); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 8924b49d496ebb4a262bb2fecaaed432b2e29734..454c6a3c3ee4e5cad8389788ab960e51408e606f 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -157,16 +157,8 @@ impl ChatPanel { .channel_at_index(selected_ix) .map(|e| e.1.id); if let Some(selected_channel_id) = selected_channel_id { - let open_chat = this.channel_store.update(cx, |store, cx| { - store.open_channel_chat(selected_channel_id, cx) - }); - cx.spawn(|this, mut cx| async move { - let chat = open_chat.await?; - this.update(&mut cx, |this, cx| { - this.set_active_channel(chat, cx); - }) - }) - .detach_and_log_err(cx); + this.select_channel(selected_channel_id, cx) + .detach_and_log_err(cx); } }) .detach(); @@ -230,22 +222,24 @@ impl ChatPanel { }); } - fn set_active_channel( - &mut self, - channel: ModelHandle, - cx: &mut ViewContext, - ) { - if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) { + fn set_active_channel(&mut self, chat: ModelHandle, cx: &mut ViewContext) { + if self.active_channel.as_ref().map(|e| &e.0) != Some(&chat) { + let id = chat.read(cx).channel().id; { - let channel = channel.read(cx); - self.message_list.reset(channel.message_count()); - let placeholder = format!("Message #{}", channel.name()); + let chat = chat.read(cx); + self.message_list.reset(chat.message_count()); + let placeholder = format!("Message #{}", chat.channel().name); self.input_editor.update(cx, move |editor, cx| { editor.set_placeholder_text(placeholder, cx); }); } - let subscription = cx.subscribe(&channel, Self::channel_did_change); - self.active_channel = Some((channel, subscription)); + let subscription = cx.subscribe(&chat, Self::channel_did_change); + self.active_channel = Some((chat, subscription)); + self.channel_select.update(cx, |select, cx| { + if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) { + select.set_selected_index(ix, cx); + } + }); } } @@ -424,6 +418,22 @@ impl ChatPanel { }) } } + + pub fn select_channel( + &mut self, + selected_channel_id: u64, + cx: &mut ViewContext, + ) -> Task> { + let open_chat = self.channel_store.update(cx, |store, cx| { + store.open_channel_chat(selected_channel_id, cx) + }); + cx.spawn(|this, mut cx| async move { + let chat = open_chat.await?; + this.update(&mut cx, |this, cx| { + this.set_active_channel(chat, cx); + }) + }) + } } impl Entity for ChatPanel { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 6198acdec26dcf1a3a4524ce8e5b66ba9d7d8910..d1adfc7002a838df7021f574d1e53d3819ad670a 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -3,6 +3,7 @@ mod contact_finder; use crate::{ channel_view::{self, ChannelView}, + chat_panel::ChatPanel, face_pile::FacePile, CollaborationPanelSettings, }; @@ -203,7 +204,7 @@ enum Section { #[derive(Clone, Debug)] enum ListEntry { - Header(Section, usize), + Header(Section), CallParticipant { user: Arc, is_pending: bool, @@ -225,6 +226,10 @@ enum ListEntry { channel: Arc, depth: usize, }, + ChannelCall { + channel: Arc, + depth: usize, + }, ChannelNotes { channel_id: ChannelId, }, @@ -269,7 +274,7 @@ impl CollabPanel { this.selection = this .entries .iter() - .position(|entry| !matches!(entry, ListEntry::Header(_, _))); + .position(|entry| !matches!(entry, ListEntry::Header(_))); } } }) @@ -305,16 +310,9 @@ impl CollabPanel { let current_project_id = this.project.read(cx).remote_id(); match &this.entries[ix] { - ListEntry::Header(section, depth) => { + ListEntry::Header(section) => { let is_collapsed = this.collapsed_sections.contains(section); - this.render_header( - *section, - &theme, - *depth, - is_selected, - is_collapsed, - cx, - ) + this.render_header(*section, &theme, is_selected, is_collapsed, cx) } ListEntry::CallParticipant { user, is_pending } => { Self::render_call_participant( @@ -371,6 +369,13 @@ impl CollabPanel { return channel_row; } } + ListEntry::ChannelCall { channel, depth } => this.render_channel_call( + &*channel, + *depth, + &theme.collab_panel, + is_selected, + cx, + ), ListEntry::ChannelNotes { channel_id } => this.render_channel_notes( *channel_id, &theme.collab_panel, @@ -558,7 +563,7 @@ impl CollabPanel { let old_entries = mem::take(&mut self.entries); if let Some(room) = ActiveCall::global(cx).read(cx).room() { - self.entries.push(ListEntry::Header(Section::ActiveCall, 0)); + self.entries.push(ListEntry::Header(Section::ActiveCall)); if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); @@ -673,7 +678,7 @@ impl CollabPanel { let mut request_entries = Vec::new(); if cx.has_flag::() { - self.entries.push(ListEntry::Header(Section::Channels, 0)); + self.entries.push(ListEntry::Header(Section::Channels)); if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { self.match_candidates.clear(); @@ -746,6 +751,12 @@ impl CollabPanel { channel: channel.clone(), depth, }); + if !channel_store.channel_participants(channel.id).is_empty() { + self.entries.push(ListEntry::ChannelCall { + channel: channel.clone(), + depth, + }); + } } } } @@ -776,7 +787,7 @@ impl CollabPanel { if !request_entries.is_empty() { self.entries - .push(ListEntry::Header(Section::ChannelInvites, 1)); + .push(ListEntry::Header(Section::ChannelInvites)); if !self.collapsed_sections.contains(&Section::ChannelInvites) { self.entries.append(&mut request_entries); } @@ -784,7 +795,7 @@ impl CollabPanel { } } - self.entries.push(ListEntry::Header(Section::Contacts, 0)); + self.entries.push(ListEntry::Header(Section::Contacts)); request_entries.clear(); let incoming = user_store.incoming_contact_requests(); @@ -847,7 +858,7 @@ impl CollabPanel { if !request_entries.is_empty() { self.entries - .push(ListEntry::Header(Section::ContactRequests, 1)); + .push(ListEntry::Header(Section::ContactRequests)); if !self.collapsed_sections.contains(&Section::ContactRequests) { self.entries.append(&mut request_entries); } @@ -886,7 +897,7 @@ impl CollabPanel { (offline_contacts, Section::Offline), ] { if !matches.is_empty() { - self.entries.push(ListEntry::Header(section, 1)); + self.entries.push(ListEntry::Header(section)); if !self.collapsed_sections.contains(§ion) { let active_call = &ActiveCall::global(cx).read(cx); for mat in matches { @@ -1174,7 +1185,6 @@ impl CollabPanel { &self, section: Section, theme: &theme::Theme, - depth: usize, is_selected: bool, is_collapsed: bool, cx: &mut ViewContext, @@ -1282,7 +1292,13 @@ impl CollabPanel { _ => None, }; - let can_collapse = depth > 0; + let can_collapse = match section { + Section::ActiveCall | Section::Channels | Section::Contacts => false, + Section::ChannelInvites + | Section::ContactRequests + | Section::Online + | Section::Offline => true, + }; let icon_size = (&theme.collab_panel).section_icon_size; let mut result = MouseEventHandler::new::(section as usize, cx, |state, _| { let header_style = if can_collapse { @@ -1550,17 +1566,8 @@ impl CollabPanel { let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok()); - let is_active = iife!({ - let call_channel = ActiveCall::global(cx) - .read(cx) - .room()? - .read(cx) - .channel_id()?; - Some(call_channel == channel_id) - }) - .unwrap_or(false); - - const FACEPILE_LIMIT: usize = 3; + enum ChannelCall {} + enum ChannelNotes {} MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { Flex::::row() @@ -1580,37 +1587,32 @@ impl CollabPanel { .left() .flex(1., true), ) - .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); - - Some( - FacePile::new(theme.face_overlap) - .with_children( - participants - .iter() - .filter_map(|user| { - Some( - Image::from_data(user.avatar.clone()?) - .with_style(theme.channel_avatar), - ) - }) - .take(FACEPILE_LIMIT), - ) - .with_children((extra_count > 0).then(|| { - Label::new( - format!("+{}", extra_count), - theme.extra_participant_label.text.clone(), - ) - .contained() - .with_style(theme.extra_participant_label.container) - })), - ) - } else { - None - } - }) + .with_child( + MouseEventHandler::new::(channel_id as usize, cx, |_, _| { + Svg::new("icons/radix/speaker-loud.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .right() + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.join_channel_call(channel_id, cx) + }), + ) + .with_child( + MouseEventHandler::new::(channel_id as usize, cx, |_, _| { + Svg::new("icons/radix/file.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .right() + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx); + }), + ) .align_children_center() .styleable_component() .disclosable(disclosed, Box::new(ToggleCollapse { channel_id })) @@ -1620,14 +1622,102 @@ impl CollabPanel { .constrained() .with_height(theme.row_height) .contained() - .with_style(*theme.channel_row.style_for(is_selected || is_active, state)) + .with_style(*theme.channel_row.style_for(is_selected, state)) .with_padding_left( theme.channel_row.default_style().padding.left + theme.channel_indent * depth as f32, ) }) .on_click(MouseButton::Left, move |_, this, cx| { - this.join_channel(channel_id, cx); + this.join_channel_chat(channel_id, cx); + }) + .on_click(MouseButton::Right, move |e, this, cx| { + this.deploy_channel_context_menu(Some(e.position), channel_id, cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .into_any() + } + + fn render_channel_call( + &self, + channel: &Channel, + depth: usize, + theme: &theme::CollabPanel, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + let channel_id = channel.id; + + let is_active = iife!({ + let call_channel = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + Some(call_channel == channel_id) + }) + .unwrap_or(false); + + const FACEPILE_LIMIT: usize = 5; + + enum ChannelCall {} + + let host_avatar_width = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + + MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { + let participants = self.channel_store.read(cx).channel_participants(channel_id); + let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); + let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); + let row = theme.project_row.in_state(is_selected).style_for(state); + + Flex::::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + true, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) + .with_child( + FacePile::new(theme.face_overlap) + .with_children( + participants + .iter() + .filter_map(|user| { + Some( + Image::from_data(user.avatar.clone()?) + .with_style(theme.channel_avatar), + ) + }) + .take(FACEPILE_LIMIT), + ) + .with_children((extra_count > 0).then(|| { + Label::new( + format!("+{}", extra_count), + theme.extra_participant_label.text.clone(), + ) + .contained() + .with_style(theme.extra_participant_label.container) + })), + ) + .align_children_center() + .constrained() + .with_height(theme.row_height) + .aligned() + .left() + .contained() + .with_style(*theme.channel_row.style_for(is_selected || is_active, state)) + .with_padding_left( + theme.channel_row.default_style().padding.left + + theme.channel_indent * (depth + 1) as f32, + ) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.join_channel_call(channel_id, cx); }) .on_click(MouseButton::Right, move |e, this, cx| { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); @@ -1982,7 +2072,7 @@ impl CollabPanel { if let Some(selection) = self.selection { if let Some(entry) = self.entries.get(selection) { match entry { - ListEntry::Header(section, _) => match section { + 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), @@ -2022,7 +2112,10 @@ impl CollabPanel { } } ListEntry::Channel { channel, .. } => { - self.join_channel(channel.id, cx); + self.join_channel_chat(channel.id, cx); + } + ListEntry::ChannelCall { channel, .. } => { + self.join_channel_call(channel.id, cx); } ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), _ => {} @@ -2411,11 +2504,25 @@ impl CollabPanel { .detach_and_log_err(cx); } - fn join_channel(&self, channel: u64, cx: &mut ViewContext) { + fn join_channel_call(&self, channel: u64, cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |call, cx| call.join_channel(channel, cx)) .detach_and_log_err(cx); } + + fn join_channel_chat(&self, channel_id: u64, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade(cx) { + cx.app_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, cx).detach_and_log_err(cx); + }); + } + }); + }); + } + } } fn render_tree_branch( @@ -2606,9 +2713,9 @@ impl Panel for CollabPanel { impl PartialEq for ListEntry { fn eq(&self, other: &Self) -> bool { match self { - ListEntry::Header(section_1, depth_1) => { - if let ListEntry::Header(section_2, depth_2) = other { - return section_1 == section_2 && depth_1 == depth_2; + ListEntry::Header(section_1) => { + if let ListEntry::Header(section_2) = other { + return section_1 == section_2; } } ListEntry::CallParticipant { user: user_1, .. } => { @@ -2650,6 +2757,18 @@ impl PartialEq for ListEntry { return channel_1.id == channel_2.id && depth_1 == depth_2; } } + ListEntry::ChannelCall { + channel: channel_1, + depth: depth_1, + } => { + if let ListEntry::ChannelCall { + channel: channel_2, + depth: depth_2, + } = other + { + return channel_1.id == channel_2.id && depth_1 == depth_2; + } + } ListEntry::ChannelNotes { channel_id } => { if let ListEntry::ChannelNotes { channel_id: other_id, diff --git a/crates/gpui/src/views.rs b/crates/gpui/src/views.rs index fa9a63b3d3476371eec0791bd08c93874d35c4e2..c93e5acd1225104f36bd12e16261a66481af2fdd 100644 --- a/crates/gpui/src/views.rs +++ b/crates/gpui/src/views.rs @@ -2,6 +2,4 @@ mod select; pub use select::{ItemType, Select, SelectStyle}; -pub fn init(cx: &mut super::AppContext) { - select::init(cx); -} +pub fn init(_: &mut super::AppContext) {} diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index f76fab738e385714db63486f0f44cd14b6fd862f..0de535f8375730bd29a20fc155a76e4d11568107 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -1,8 +1,5 @@ -use serde::Deserialize; - use crate::{ - actions, elements::*, impl_actions, platform::MouseButton, AppContext, Entity, View, - ViewContext, WeakViewHandle, + elements::*, platform::MouseButton, AppContext, Entity, View, ViewContext, WeakViewHandle, }; pub struct Select { @@ -27,19 +24,8 @@ pub enum ItemType { Unselected, } -#[derive(Clone, Deserialize, PartialEq)] -pub struct SelectItem(pub usize); - -actions!(select, [ToggleSelect]); -impl_actions!(select, [SelectItem]); - pub enum Event {} -pub fn init(cx: &mut AppContext) { - cx.add_action(Select::toggle); - cx.add_action(Select::select_item); -} - impl Select { pub fn new AnyElement>( item_count: usize, @@ -67,13 +53,13 @@ impl Select { cx.notify(); } - fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext) { + fn toggle(&mut self, cx: &mut ViewContext) { self.is_open = !self.is_open; cx.notify(); } - fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext) { - self.selected_item_ix = action.0; + pub fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + self.selected_item_ix = ix; self.is_open = false; cx.notify(); } @@ -117,7 +103,7 @@ impl View for Select { .with_style(style.header) }) .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle(&Default::default(), cx); + this.toggle(cx); }), ); if self.is_open { @@ -143,7 +129,7 @@ impl View for Select { ) }) .on_click(MouseButton::Left, move |_, this, cx| { - this.select_item(&SelectItem(ix), cx); + this.set_selected_index(ix, cx); }) .into_any() }))