join channels (#3082)

Release Notes:

- Clicking on a channel in the sidebar will now join the channel and
open the notes
- If you join a channel that already shared projects, you will join the
projects automatically and follow the host.
- Clicking on the current channel in the sidebar will re-open the notes.
- Chat can now be accessed from the right click menu of channels.


- (probably not worth mentioning) Various improvements to hover states
and tooltips in the collab ui; and if you click on a channel while in
another call, confirm before switching.
This commit is contained in:
Conrad Irwin 2023-10-03 13:54:11 -06:00 committed by GitHub
commit 55d2b9b3c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 306 additions and 99 deletions

View file

@ -291,10 +291,10 @@ impl ActiveCall {
&mut self,
channel_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
) -> Task<Result<ModelHandle<Room>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(()));
return Task::ready(Ok(room));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
}
@ -309,7 +309,7 @@ impl ActiveCall {
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
});
Ok(())
Ok(room)
})
}

View file

@ -594,6 +594,33 @@ impl Room {
.map_or(&[], |v| v.as_slice())
}
/// projects_to_join returns a list of shared projects sorted such
/// that the most 'active' projects appear last.
pub fn projects_to_join(&self) -> Vec<(u64, u64)> {
let mut projects = HashMap::default();
let mut hosts = HashMap::default();
for participant in self.remote_participants.values() {
match participant.location {
ParticipantLocation::SharedProject { project_id } => {
*projects.entry(project_id).or_insert(0) += 1;
}
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
}
for project in &participant.projects {
*projects.entry(project.id).or_insert(0) += 1;
hosts.insert(project.id, participant.user.id);
}
}
let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
pairs.sort_by_key(|(_, count)| 0 - *count as i32);
pairs
.into_iter()
.map(|(project_id, _)| (project_id, hosts[&project_id]))
.collect()
}
async fn handle_room_updated(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::RoomUpdated>,

View file

@ -95,6 +95,11 @@ pub struct JoinChannelCall {
pub channel_id: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct JoinChannelChat {
pub channel_id: u64,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct StartMoveChannelFor {
channel_id: ChannelId,
@ -151,6 +156,7 @@ impl_actions!(
ToggleCollapse,
OpenChannelNotes,
JoinChannelCall,
JoinChannelChat,
LinkChannel,
StartMoveChannelFor,
StartLinkChannelFor,
@ -198,6 +204,7 @@ pub fn init(cx: &mut AppContext) {
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(
|panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
@ -471,6 +478,12 @@ impl CollabPanel {
.iter()
.position(|entry| !matches!(entry, ListEntry::Header(_)));
}
} else if let editor::Event::Blurred = event {
let query = this.filter_editor.read(cx).text(cx);
if query.is_empty() {
this.selection.take();
this.update_entries(true, cx);
}
}
})
.detach();
@ -555,7 +568,7 @@ impl CollabPanel {
&*channel,
*depth,
path.to_owned(),
&theme.collab_panel,
&theme,
is_selected,
ix,
cx,
@ -608,7 +621,7 @@ impl CollabPanel {
contact,
*calling,
&this.project,
&theme.collab_panel,
&theme,
is_selected,
cx,
),
@ -768,9 +781,16 @@ impl CollabPanel {
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);
@ -1138,44 +1158,49 @@ impl CollabPanel {
}
let old_scroll_top = self.list_state.logical_scroll_top();
self.list_state.reset(self.entries.len());
// 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 {
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: 0.,
offset_in_item: old_scroll_top.offset_in_item,
})
})
.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: 0.,
.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: 0.,
})
})
});
.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: 0.,
})
});
self.list_state
.scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
self.list_state
.scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
}
}
cx.notify();
@ -1633,16 +1658,20 @@ impl CollabPanel {
contact: &Contact,
calling: bool,
project: &ModelHandle<Project>,
theme: &theme::CollabPanel,
theme: &theme::Theme,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum ContactTooltip {}
let collab_theme = &theme.collab_panel;
let online = contact.online;
let busy = contact.busy || calling;
let user_id = contact.user.id;
let github_login = contact.user.github_login.clone();
let initial_project = project.clone();
let mut event_handler =
let event_handler =
MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
Flex::row()
.with_children(contact.user.avatar.clone().map(|avatar| {
@ -1652,9 +1681,9 @@ impl CollabPanel {
.collapsed()
.contained()
.with_style(if busy {
theme.contact_status_busy
collab_theme.contact_status_busy
} else {
theme.contact_status_free
collab_theme.contact_status_free
})
.aligned(),
)
@ -1664,7 +1693,7 @@ impl CollabPanel {
Stack::new()
.with_child(
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.with_style(collab_theme.contact_avatar)
.aligned()
.left(),
)
@ -1673,58 +1702,94 @@ impl CollabPanel {
.with_child(
Label::new(
contact.user.github_login.clone(),
theme.contact_username.text.clone(),
collab_theme.contact_username.text.clone(),
)
.contained()
.with_style(theme.contact_username.container)
.with_style(collab_theme.contact_username.container)
.aligned()
.left()
.flex(1., true),
)
.with_child(
MouseEventHandler::new::<Cancel, _>(
contact.user.id as usize,
cx,
|mouse_state, _| {
let button_style = theme.contact_button.style_for(mouse_state);
render_icon_button(button_style, "icons/x.svg")
.aligned()
.flex_float()
},
.with_children(if state.hovered() {
Some(
MouseEventHandler::new::<Cancel, _>(
contact.user.id as usize,
cx,
|mouse_state, _| {
let button_style =
collab_theme.contact_button.style_for(mouse_state);
render_icon_button(button_style, "icons/x.svg")
.aligned()
.flex_float()
},
)
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.remove_contact(user_id, &github_login, cx);
})
.flex_float(),
)
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.remove_contact(user_id, &github_login, cx);
})
.flex_float(),
)
} else {
None
})
.with_children(if calling {
Some(
Label::new("Calling", theme.calling_indicator.text.clone())
Label::new("Calling", collab_theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.with_style(collab_theme.calling_indicator.container)
.aligned(),
)
} else {
None
})
.constrained()
.with_height(theme.row_height)
.with_height(collab_theme.row_height)
.contained()
.with_style(*theme.contact_row.in_state(is_selected).style_for(state))
})
.on_click(MouseButton::Left, move |_, this, cx| {
if online && !busy {
this.call(user_id, Some(initial_project.clone()), cx);
}
.with_style(
*collab_theme
.contact_row
.in_state(is_selected)
.style_for(state),
)
});
if online {
event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
}
if online && !busy {
let room = ActiveCall::global(cx).read(cx).room();
let label = if room.is_some() {
format!("Invite {} to join call", contact.user.github_login)
} else {
format!("Call {}", contact.user.github_login)
};
event_handler.into_any()
event_handler
.on_click(MouseButton::Left, move |_, this, cx| {
this.call(user_id, Some(initial_project.clone()), cx);
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<ContactTooltip>(
contact.user.id as usize,
label,
None,
theme.tooltip.clone(),
cx,
)
.into_any()
} else {
event_handler
.with_tooltip::<ContactTooltip>(
contact.user.id as usize,
format!(
"{} is {}",
contact.user.github_login,
if busy { "on a call" } else { "offline" }
),
None,
theme.tooltip.clone(),
cx,
)
.into_any()
}
}
fn render_contact_placeholder(
@ -1827,12 +1892,13 @@ impl CollabPanel {
channel: &Channel,
depth: usize,
path: ChannelPath,
theme: &theme::CollabPanel,
theme: &theme::Theme,
is_selected: bool,
ix: usize,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let channel_id = channel.id;
let collab_theme = &theme.collab_panel;
let has_children = self.channel_store.read(cx).has_children(channel_id);
let other_selected =
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
@ -1851,6 +1917,8 @@ impl CollabPanel {
const FACEPILE_LIMIT: usize = 3;
enum ChannelCall {}
enum IconTooltip {}
enum ChannelTooltip {}
let mut is_dragged_over = false;
if cx
@ -1886,18 +1954,29 @@ impl CollabPanel {
Flex::<Self>::row()
.with_child(
Svg::new("icons/hash.svg")
.with_color(theme.channel_hash.color)
.with_color(collab_theme.channel_hash.color)
.constrained()
.with_width(theme.channel_hash.width)
.with_width(collab_theme.channel_hash.width)
.aligned()
.left(),
)
.with_child(
Label::new(channel.name.clone(), theme.channel_name.text.clone())
Label::new(channel.name.clone(), collab_theme.channel_name.text.clone())
.contained()
.with_style(theme.channel_name.container)
.with_style(collab_theme.channel_name.container)
.aligned()
.left()
.with_tooltip::<ChannelTooltip>(
channel_id as usize,
if is_active {
"Open channel notes"
} else {
"Join channel"
},
None,
theme.tooltip.clone(),
cx,
)
.flex(1., true),
)
.with_child(
@ -1907,14 +1986,14 @@ impl CollabPanel {
if !participants.is_empty() {
let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
FacePile::new(theme.face_overlap)
FacePile::new(collab_theme.face_overlap)
.with_children(
participants
.iter()
.filter_map(|user| {
Some(
Image::from_data(user.avatar.clone()?)
.with_style(theme.channel_avatar),
.with_style(collab_theme.channel_avatar),
)
})
.take(FACEPILE_LIMIT),
@ -1922,24 +2001,48 @@ impl CollabPanel {
.with_children((extra_count > 0).then(|| {
Label::new(
format!("+{}", extra_count),
theme.extra_participant_label.text.clone(),
collab_theme.extra_participant_label.text.clone(),
)
.contained()
.with_style(theme.extra_participant_label.container)
.with_style(collab_theme.extra_participant_label.container)
}))
.with_tooltip::<IconTooltip>(
channel_id as usize,
if is_active {
"Open Channel Notes"
} else {
"Join channel"
},
None,
theme.tooltip.clone(),
cx,
)
.into_any()
} else if row_hovered {
Svg::new("icons/speaker-loud.svg")
.with_color(theme.channel_hash.color)
Svg::new("icons/file.svg")
.with_color(collab_theme.channel_hash.color)
.constrained()
.with_width(theme.channel_hash.width)
.with_width(collab_theme.channel_hash.width)
.with_tooltip::<IconTooltip>(
channel_id as usize,
"Open channel notes",
None,
theme.tooltip.clone(),
cx,
)
.into_any()
} else {
Empty::new().into_any()
}
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.join_channel_call(channel_id, cx);
let participants =
this.channel_store.read(cx).channel_participants(channel_id);
if is_active || participants.is_empty() {
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
} else {
this.join_channel(channel_id, cx);
};
}),
)
.align_children_center()
@ -1951,24 +2054,28 @@ impl CollabPanel {
}),
)
.with_id(ix)
.with_style(theme.disclosure.clone())
.with_style(collab_theme.disclosure.clone())
.element()
.constrained()
.with_height(theme.row_height)
.with_height(collab_theme.row_height)
.contained()
.with_style(select_state(
theme
collab_theme
.channel_row
.in_state(is_selected || is_active || is_dragged_over),
))
.with_padding_left(
theme.channel_row.default_style().padding.left
+ theme.channel_indent * depth as f32,
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.take().is_none() {
this.join_channel_chat(channel_id, cx);
if is_active {
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
} else {
this.join_channel(channel_id, cx)
}
}
})
.on_click(MouseButton::Right, {
@ -2392,6 +2499,13 @@ impl CollabPanel {
},
));
items.push(ContextMenuItem::action(
"Open Chat",
JoinChannelChat {
channel_id: path.channel_id(),
},
));
if self.channel_store.read(cx).is_user_admin(path.channel_id()) {
let parent_id = path.parent_id();
@ -2588,7 +2702,28 @@ impl CollabPanel {
}
}
ListEntry::Channel { channel, .. } => {
self.join_channel_chat(channel.id, cx);
let is_active = iife!({
let call_channel = ActiveCall::global(cx)
.read(cx)
.room()?
.read(cx)
.channel_id()?;
dbg!(call_channel, channel.id);
Some(call_channel == channel.id)
})
.unwrap_or(false);
dbg!(is_active);
if is_active {
self.open_channel_notes(
&OpenChannelNotes {
channel_id: channel.id,
},
cx,
)
} else {
self.join_channel(channel.id, cx)
}
}
ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
_ => {}
@ -2991,13 +3126,58 @@ impl CollabPanel {
.detach_and_log_err(cx);
}
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(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone();
let window = cx.window();
let active_call = ActiveCall::global(cx);
cx.spawn(|_, mut cx| async move {
if active_call.read_with(&mut cx, |active_call, _| active_call.room().is_some()) {
let answer = window.prompt(
PromptLevel::Warning,
"Do you want to leave the current call?",
&["Yes, Join Channel", "Cancel"],
&mut cx,
);
if let Some(mut answer) = answer {
if answer.next().await == Some(1) {
return anyhow::Ok(());
}
}
}
let room = active_call
.update(&mut cx, |call, cx| call.join_channel(channel_id, cx))
.await?;
let tasks = room.update(&mut cx, |room, cx| {
let Some(workspace) = workspace.upgrade(cx) else {
return vec![];
};
let projects = room.projects_to_join();
if projects.is_empty() {
ChannelView::open(channel_id, workspace, cx).detach();
return vec![];
}
room.projects_to_join()
.into_iter()
.map(|(project_id, user_id)| {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, user_id, app_state, cx)
})
.collect()
});
for task in tasks {
task.await?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn join_channel_chat(&mut self, channel_id: u64, cx: &mut ViewContext<Self>) {
fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
let channel_id = action.channel_id;
if let Some(workspace) = self.workspace.upgrade(cx) {
cx.app_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {