single click channel (#7596)

- Open channel notes and chat on channel click
- WIP
- Fix compile error
- Don't join live kit until requested
- Track in_call state separately from in_room



Release Notes:

- Improved channels: you can now be in a channel without joining the
audio call automatically

**or**

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Conrad Irwin 2024-02-09 14:18:27 -07:00 committed by GitHub
parent 2b39a9512a
commit efe23ebfcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 659 additions and 489 deletions

View file

@ -40,7 +40,7 @@ use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
OpenChannelNotes, Workspace,
Workspace,
};
actions!(
@ -69,19 +69,6 @@ pub fn init(cx: &mut AppContext) {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<CollabPanel>(cx);
});
workspace.register_action(|_, _: &OpenChannelNotes, cx| {
let channel_id = ActiveCall::global(cx)
.read(cx)
.room()
.and_then(|room| room.read(cx).channel_id());
if let Some(channel_id) = channel_id {
let workspace = cx.view().clone();
cx.window_context().defer(move |cx| {
ChannelView::open(channel_id, None, workspace, cx).detach_and_log_err(cx)
});
}
});
})
.detach();
}
@ -175,6 +162,9 @@ enum ListEntry {
depth: usize,
has_children: bool,
},
ChannelCall {
channel_id: ChannelId,
},
ChannelNotes {
channel_id: ChannelId,
},
@ -382,6 +372,7 @@ impl CollabPanel {
if query.is_empty() {
if let Some(channel_id) = room.channel_id() {
self.entries.push(ListEntry::ChannelCall { channel_id });
self.entries.push(ListEntry::ChannelNotes { channel_id });
self.entries.push(ListEntry::ChannelChat { channel_id });
}
@ -479,7 +470,7 @@ impl CollabPanel {
&& participant.video_tracks.is_empty(),
});
}
if !participant.video_tracks.is_empty() {
if room.in_call() && !participant.video_tracks.is_empty() {
self.entries.push(ListEntry::ParticipantScreen {
peer_id: Some(participant.peer_id),
is_last: true,
@ -832,8 +823,6 @@ impl CollabPanel {
cx: &mut ViewContext<Self>,
) -> ListItem {
let user_id = user.id;
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);
let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
@ -846,12 +835,6 @@ impl CollabPanel {
.selected(is_selected)
.end_slot(if is_pending {
Label::new("Calling").color(Color::Muted).into_any_element()
} else if is_current_user {
IconButton::new("leave-call", IconName::Exit)
.style(ButtonStyle::Subtle)
.on_click(move |_, cx| Self::leave_call(cx))
.tooltip(|cx| Tooltip::text("Leave Call", cx))
.into_any_element()
} else if role == proto::ChannelRole::Guest {
Label::new("Guest").color(Color::Muted).into_any_element()
} else {
@ -953,12 +936,88 @@ impl CollabPanel {
}
}
fn render_channel_call(
&self,
channel_id: ChannelId,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let (is_in_call, call_participants) = ActiveCall::global(cx)
.read(cx)
.room()
.map(|room| (room.read(cx).in_call(), room.read(cx).call_participants(cx)))
.unwrap_or_default();
const FACEPILE_LIMIT: usize = 3;
let face_pile = if !call_participants.is_empty() {
let extra_count = call_participants.len().saturating_sub(FACEPILE_LIMIT);
let result = FacePile::new(
call_participants
.iter()
.map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
.take(FACEPILE_LIMIT)
.chain(if extra_count > 0 {
Some(
div()
.ml_2()
.child(Label::new(format!("+{extra_count}")))
.into_any_element(),
)
} else {
None
})
.collect::<SmallVec<_>>(),
);
Some(result)
} else {
None
};
ListItem::new("channel-call")
.selected(is_selected)
.start_slot(
h_flex()
.gap_1()
.child(render_tree_branch(false, true, cx))
.child(IconButton::new(0, IconName::AudioOn)),
)
.when(is_in_call, |el| {
el.end_slot(
IconButton::new(1, IconName::Exit)
.style(ButtonStyle::Filled)
.shape(ui::IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Leave call", cx))
.on_click(cx.listener(|this, _, cx| this.leave_channel_call(cx))),
)
})
.when(!is_in_call, |el| {
el.tooltip(move |cx| Tooltip::text("Join audio call", cx))
.on_click(cx.listener(move |this, _, cx| {
this.join_channel_call(channel_id, cx);
}))
})
.child(
div()
.text_ui()
.when(!call_participants.is_empty(), |el| {
el.font_weight(FontWeight::SEMIBOLD)
})
.child("call"),
)
.children(face_pile)
}
fn render_channel_notes(
&self,
channel_id: ChannelId,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
ListItem::new("channel-notes")
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
@ -970,7 +1029,14 @@ impl CollabPanel {
.child(render_tree_branch(false, true, cx))
.child(IconButton::new(0, IconName::File)),
)
.child(Label::new("notes"))
.child(
div()
.text_ui()
.when(has_notes_notification, |el| {
el.font_weight(FontWeight::SEMIBOLD)
})
.child("notes"),
)
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
}
@ -980,6 +1046,8 @@ impl CollabPanel {
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let has_messages_notification = channel_store.has_new_messages(channel_id);
ListItem::new("channel-chat")
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
@ -991,7 +1059,14 @@ impl CollabPanel {
.child(render_tree_branch(false, false, cx))
.child(IconButton::new(0, IconName::MessageBubbles)),
)
.child(Label::new("chat"))
.child(
div()
.text_ui()
.when(has_messages_notification, |el| {
el.font_weight(FontWeight::SEMIBOLD)
})
.child("chat"),
)
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
}
@ -1249,12 +1324,14 @@ impl CollabPanel {
cx: &mut ViewContext<Self>,
) {
let this = cx.view().clone();
let in_room = ActiveCall::global(cx).read(cx).room().is_some();
let room = ActiveCall::global(cx).read(cx).room();
let in_room = room.is_some();
let in_call = room.is_some_and(|room| room.read(cx).in_call());
let context_menu = ContextMenu::build(cx, |mut context_menu, _| {
let user_id = contact.user.id;
if contact.online && !contact.busy {
if contact.online && !contact.busy && (!in_room || in_call) {
let label = if in_room {
format!("Invite {} to join", contact.user.github_login)
} else {
@ -1402,7 +1479,7 @@ impl CollabPanel {
if is_active {
self.open_channel_notes(channel.id, cx)
} else {
self.join_channel(channel.id, cx)
self.open_channel(channel.id, cx)
}
}
ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
@ -1421,6 +1498,9 @@ impl CollabPanel {
ListEntry::ChannelInvite(channel) => {
self.respond_to_channel_invite(channel.id, true, cx)
}
ListEntry::ChannelCall { channel_id } => {
self.join_channel_call(*channel_id, cx)
}
ListEntry::ChannelNotes { channel_id } => {
self.open_channel_notes(*channel_id, cx)
}
@ -1883,14 +1963,14 @@ impl CollabPanel {
.detach_and_prompt_err("Call failed", cx, |_, _| None);
}
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
fn open_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
return;
};
workspace::join_channel(
workspace::open_channel(
channel_id,
workspace.read(cx).app_state().clone(),
Some(handle),
@ -1899,6 +1979,23 @@ impl CollabPanel {
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
}
fn join_channel_call(&mut self, _channel_id: ChannelId, cx: &mut ViewContext<Self>) {
let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
return;
};
room.update(cx, |room, cx| room.join_call(cx))
.detach_and_prompt_err("Failed to join call", cx, |_, _| None)
}
fn leave_channel_call(&mut self, cx: &mut ViewContext<Self>) {
let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
return;
};
room.update(cx, |room, cx| room.leave_call(cx));
}
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
@ -2024,6 +2121,9 @@ impl CollabPanel {
ListEntry::ParticipantScreen { peer_id, is_last } => self
.render_participant_screen(*peer_id, *is_last, is_selected, cx)
.into_any_element(),
ListEntry::ChannelCall { channel_id } => self
.render_channel_call(*channel_id, is_selected, cx)
.into_any_element(),
ListEntry::ChannelNotes { channel_id } => self
.render_channel_notes(*channel_id, is_selected, cx)
.into_any_element(),
@ -2089,7 +2189,6 @@ impl CollabPanel {
is_collapsed: bool,
cx: &ViewContext<Self>,
) -> impl IntoElement {
let mut channel_link = None;
let mut channel_tooltip_text = None;
let mut channel_icon = None;
@ -2100,13 +2199,12 @@ impl CollabPanel {
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."))
(Some(IconName::Public), Some("Close Channel"))
}
proto::ChannelVisibility::Members => {
(Some("icons/hash.svg"), Some("Copy private channel link."))
(Some(IconName::Hash), Some("Close Channel"))
}
};
@ -2128,17 +2226,10 @@ impl CollabPanel {
};
let button = match section {
Section::ActiveCall => channel_link.map(|channel_link| {
let channel_link_copy = channel_link.clone();
IconButton::new("channel-link", IconName::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))
Section::ActiveCall => channel_icon.map(|_| {
IconButton::new("channel-link", IconName::Close)
.on_click(move |_, cx| Self::leave_call(cx))
.tooltip(|cx| Tooltip::text("Close channel", cx))
.into_any_element()
}),
Section::Contacts => Some(
@ -2173,6 +2264,9 @@ impl CollabPanel {
this.toggle_section_expanded(section, cx);
}))
})
.when_some(channel_icon, |el, channel_icon| {
el.start_slot(Icon::new(channel_icon).color(Color::Muted))
})
.inset(true)
.end_slot::<AnyElement>(button)
.selected(is_selected),
@ -2478,11 +2572,9 @@ impl CollabPanel {
}),
)
.on_click(cx.listener(move |this, _, cx| {
if is_active {
this.open_channel_notes(channel_id, cx)
} else {
this.join_channel(channel_id, cx)
}
this.open_channel(channel_id, cx);
this.open_channel_notes(channel_id, cx);
this.join_channel_chat(channel_id, cx);
}))
.on_secondary_mouse_down(cx.listener(
move |this, event: &MouseDownEvent, cx| {
@ -2499,61 +2591,24 @@ impl CollabPanel {
.color(Color::Muted),
)
.child(
h_flex()
.id(channel_id as usize)
.child(Label::new(channel.name.clone()))
.children(face_pile.map(|face_pile| face_pile.p_1())),
h_flex().id(channel_id as usize).child(
div()
.text_ui()
.when(has_messages_notification || has_notes_notification, |el| {
el.font_weight(FontWeight::SEMIBOLD)
})
.child(channel.name.clone()),
),
),
)
.child(
.children(face_pile.map(|face_pile| {
h_flex()
.absolute()
.right(rems(0.))
.z_index(1)
.h_full()
.child(
h_flex()
.h_full()
.gap_1()
.px_1()
.child(
IconButton::new("channel_chat", IconName::MessageBubbles)
.style(ButtonStyle::Filled)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(if has_messages_notification {
Color::Default
} else {
Color::Muted
})
.on_click(cx.listener(move |this, _, cx| {
this.join_channel_chat(channel_id, cx)
}))
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
.when(!has_messages_notification, |this| {
this.visible_on_hover("")
}),
)
.child(
IconButton::new("channel_notes", IconName::File)
.style(ButtonStyle::Filled)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(if has_notes_notification {
Color::Default
} else {
Color::Muted
})
.on_click(cx.listener(move |this, _, cx| {
this.open_channel_notes(channel_id, cx)
}))
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
.when(!has_notes_notification, |this| {
this.visible_on_hover("")
}),
),
),
)
.child(face_pile.p_1())
}))
.tooltip({
let channel_store = self.channel_store.clone();
move |cx| {
@ -2757,6 +2812,14 @@ impl PartialEq for ListEntry {
return channel_1.id == channel_2.id;
}
}
ListEntry::ChannelCall { channel_id } => {
if let ListEntry::ChannelCall {
channel_id: other_id,
} = other
{
return channel_id == other_id;
}
}
ListEntry::ChannelNotes { channel_id } => {
if let ListEntry::ChannelNotes {
channel_id: other_id,
@ -2855,7 +2918,7 @@ impl Render for JoinChannelTooltip {
.read(cx)
.channel_participants(self.channel_id);
div.child(Label::new("Join Channel"))
div.child(Label::new("Open Channel"))
.children(participants.iter().map(|participant| {
h_flex()
.gap_2()

View file

@ -102,6 +102,10 @@ impl Render for CollabTitlebarItem {
room.remote_participants().values().collect::<Vec<_>>();
remote_participants.sort_by_key(|p| p.participant_index.0);
if !room.in_call() {
return this;
}
let current_user_face_pile = self.render_collaborator(
&current_user,
peer_id,
@ -133,6 +137,10 @@ impl Render for CollabTitlebarItem {
== ParticipantLocation::SharedProject { project_id }
});
if !collaborator.in_call {
return None;
}
let face_pile = self.render_collaborator(
&collaborator.user,
collaborator.peer_id,
@ -185,7 +193,7 @@ impl Render for CollabTitlebarItem {
let is_local = project.is_local();
let is_shared = is_local && project.is_shared();
let is_muted = room.is_muted();
let is_deafened = room.is_deafened().unwrap_or(false);
let is_connected_to_livekit = room.in_call();
let is_screen_sharing = room.is_screen_sharing();
let read_only = room.read_only();
@ -220,22 +228,28 @@ impl Render for CollabTitlebarItem {
)),
)
})
.child(
div()
.child(
IconButton::new("leave-call", ui::IconName::Exit)
.style(ButtonStyle::Subtle)
.tooltip(|cx| Tooltip::text("Leave call", cx))
.icon_size(IconSize::Small)
.on_click(move |_, cx| {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
}),
)
.pr_2(),
)
.when(!read_only, |this| {
.when(is_connected_to_livekit, |el| {
el.child(
div()
.child(
IconButton::new("leave-call", ui::IconName::Exit)
.style(ButtonStyle::Subtle)
.tooltip(|cx| Tooltip::text("Leave call", cx))
.icon_size(IconSize::Small)
.on_click(move |_, cx| {
ActiveCall::global(cx).update(cx, |call, cx| {
if let Some(room) = call.room() {
room.update(cx, |room, cx| {
room.leave_call(cx)
})
}
})
}),
)
.pl_2(),
)
})
.when(!read_only && is_connected_to_livekit, |this| {
this.child(
IconButton::new(
"mute-microphone",
@ -262,34 +276,7 @@ impl Render for CollabTitlebarItem {
.on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
)
})
.child(
IconButton::new(
"mute-sound",
if is_deafened {
ui::IconName::AudioOff
} else {
ui::IconName::AudioOn
},
)
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
.icon_size(IconSize::Small)
.selected(is_deafened)
.tooltip(move |cx| {
if !read_only {
Tooltip::with_meta(
"Deafen Audio",
None,
"Mic will be muted",
cx,
)
} else {
Tooltip::text("Deafen Audio", cx)
}
})
.on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
)
.when(!read_only, |this| {
.when(!read_only && is_connected_to_livekit, |this| {
this.child(
IconButton::new("screen-share", ui::IconName::Screen)
.style(ButtonStyle::Subtle)

View file

@ -22,10 +22,7 @@ pub use panel_settings::{
use settings::Settings;
use workspace::{notifications::DetachAndPromptErr, AppState};
actions!(
collab,
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
);
actions!(collab, [ToggleScreenSharing, ToggleMute, LeaveCall]);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
@ -85,12 +82,6 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
}
}
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, |room, cx| room.toggle_deafen(cx));
}
}
fn notification_window_options(
screen: Rc<dyn PlatformDisplay>,
window_size: Size<Pixels>,