Restructure collab panel to allow opening chat + notes w/ one click

This commit is contained in:
Max Brunsfeld 2023-09-08 17:06:39 -07:00
parent a91f5244a9
commit c2121c25c1
6 changed files with 234 additions and 115 deletions

View file

@ -106,8 +106,8 @@ impl ChannelChat {
}))
}
pub fn name(&self) -> &str {
&self.channel.name
pub fn channel(&self) -> &Arc<Channel> {
&self.channel
}
pub fn send_message(

View file

@ -129,6 +129,12 @@ impl ChannelStore {
self.channel_paths.len()
}
pub fn index_of_channel(&self, channel_id: ChannelId) -> Option<usize> {
self.channel_paths
.iter()
.position(|path| path.ends_with(&[channel_id]))
}
pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
self.channel_paths.iter().map(move |path| {
let id = path.last().unwrap();

View file

@ -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<ChannelChat>,
cx: &mut ViewContext<Self>,
) {
if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
fn set_active_channel(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
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<ChatPanel>,
) -> Task<Result<()>> {
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 {

View file

@ -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<User>,
is_pending: bool,
@ -225,6 +226,10 @@ enum ListEntry {
channel: Arc<Channel>,
depth: usize,
},
ChannelCall {
channel: Arc<Channel>,
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::<ChannelsAlpha>() {
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(&section) {
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<Self>,
@ -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::<Header, _>(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, _>(channel.id as usize, cx, |state, cx| {
Flex::<Self>::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::<ChannelCall, _>(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::<ChannelNotes, _>(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<Self>,
) -> AnyElement<Self> {
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::<ChannelCall, _>(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::<Self>::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<Self>) {
fn join_channel_call(&self, channel: u64, cx: &mut ViewContext<Self>) {
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<Self>) {
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::<ChatPanel>(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,

View file

@ -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) {}

View file

@ -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<F: 'static + Fn(usize, ItemType, bool, &AppContext) -> AnyElement<Self>>(
item_count: usize,
@ -67,13 +53,13 @@ impl Select {
cx.notify();
}
fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext<Self>) {
fn toggle(&mut self, cx: &mut ViewContext<Self>) {
self.is_open = !self.is_open;
cx.notify();
}
fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext<Self>) {
self.selected_item_ix = action.0;
pub fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
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()
}))