Channel joining (#3428)

- Remove debugging
- Basic channel joining!

[[PR Description]]

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2023-11-28 17:00:21 -07:00 committed by GitHub
commit 7677998470
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 211 additions and 115 deletions

View File

@ -14,8 +14,8 @@ use client::{
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
View, ViewContext, VisualContext, WeakModel, WeakView,
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle,
};
pub use participant::ParticipantLocation;
use postage::watch;
@ -334,12 +334,55 @@ impl ActiveCall {
pub fn join_channel(
&mut self,
channel_id: u64,
requesting_window: Option<WindowHandle<Workspace>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
return cx.spawn(|_, _| async move {
todo!();
// let future = room.update(&mut cx, |room, cx| {
// room.most_active_project(cx).map(|(host, project)| {
// room.join_project(project, host, app_state.clone(), cx)
// })
// })
// if let Some(future) = future {
// future.await?;
// }
// Ok(Some(room))
});
}
let should_prompt = room.update(cx, |room, _| {
room.channel_id().is_some()
&& room.is_sharing_project()
&& room.remote_participants().len() > 0
});
if should_prompt && requesting_window.is_some() {
return cx.spawn(|this, mut cx| async move {
let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
cx.prompt(
PromptLevel::Warning,
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
&["Yes, Join Channel", "Cancel"],
)
})?;
if answer.await? == 1 {
return Ok(None);
}
room.update(&mut cx, |room, cx| room.clear_state(cx))?;
this.update(&mut cx, |this, cx| {
this.join_channel(channel_id, requesting_window, cx)
})?
.await
});
}
if room.read(cx).channel_id().is_some() {
room.update(cx, |room, cx| room.clear_state(cx));
}
}

View File

@ -364,7 +364,8 @@ async fn test_joining_channel_ancestor_member(
let active_call_b = cx_b.read(ActiveCall::global);
assert!(active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
.update(cx_b, |active_call, cx| active_call
.join_channel(sub_id, None, cx))
.await
.is_ok());
}
@ -394,7 +395,9 @@ async fn test_channel_room(
let active_call_b = cx_b.read(ActiveCall::global);
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.update(cx_a, |active_call, cx| {
active_call.join_channel(zed_id, None, cx)
})
.await
.unwrap();
@ -442,7 +445,9 @@ async fn test_channel_room(
});
active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
.update(cx_b, |active_call, cx| {
active_call.join_channel(zed_id, None, cx)
})
.await
.unwrap();
@ -559,12 +564,16 @@ async fn test_channel_room(
});
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.update(cx_a, |active_call, cx| {
active_call.join_channel(zed_id, None, cx)
})
.await
.unwrap();
active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
.update(cx_b, |active_call, cx| {
active_call.join_channel(zed_id, None, cx)
})
.await
.unwrap();
@ -608,7 +617,9 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
let active_call_a = cx_a.read(ActiveCall::global);
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.update(cx_a, |active_call, cx| {
active_call.join_channel(zed_id, None, cx)
})
.await
.unwrap();
@ -627,7 +638,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
active_call_a
.update(cx_a, |active_call, cx| {
active_call.join_channel(rust_id, cx)
active_call.join_channel(rust_id, None, cx)
})
.await
.unwrap();
@ -793,7 +804,7 @@ async fn test_call_from_channel(
let active_call_b = cx_b.read(ActiveCall::global);
active_call_a
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
.update(cx_a, |call, cx| call.join_channel(channel_id, None, cx))
.await
.unwrap();
@ -1286,7 +1297,7 @@ async fn test_guest_access(
// Non-members should not be allowed to join
assert!(active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
.update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
.await
.is_err());
@ -1308,7 +1319,7 @@ async fn test_guest_access(
// Client B joins channel A as a guest
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
.update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
.await
.unwrap();
@ -1341,7 +1352,7 @@ async fn test_guest_access(
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_b, cx))
.update(cx_b, |call, cx| call.join_channel(channel_b, None, cx))
.await
.unwrap();
@ -1372,7 +1383,7 @@ async fn test_invite_access(
// should not be allowed to join
assert!(active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
.update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
.await
.is_err());
@ -1390,7 +1401,7 @@ async fn test_invite_access(
.unwrap();
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
.update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
.await
.unwrap();

View File

@ -510,9 +510,10 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
// Simultaneously join channel 1 and then channel 2
active_call_a
.update(cx_a, |call, cx| call.join_channel(channel_1, cx))
.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx))
.detach();
let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
let join_channel_2 =
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx));
join_channel_2.await.unwrap();
@ -538,7 +539,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
call.invite(client_c.user_id().unwrap(), None, cx)
});
let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
let join_channel =
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
b_invite.await.unwrap();
c_invite.await.unwrap();
@ -567,7 +569,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
.unwrap();
// Simultaneously join channel 1 and call user B and user C from client A.
let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
let join_channel =
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
let b_invite = active_call_a.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)

View File

@ -90,10 +90,10 @@ use rpc::proto;
// channel_id: ChannelId,
// }
// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
// pub struct OpenChannelNotes {
// pub channel_id: ChannelId,
// }
#[derive(Action, PartialEq, Debug, Clone, Serialize, Deserialize)]
pub struct OpenChannelNotes {
pub channel_id: ChannelId,
}
// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
// pub struct JoinChannelCall {
@ -167,10 +167,10 @@ use editor::Editor;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, div, img, prelude::*, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter,
FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement,
Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
WeakView,
actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext, Div,
EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model,
ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext,
VisualContext, WeakView,
};
use project::Fs;
use serde_derive::{Deserialize, Serialize};
@ -322,17 +322,17 @@ pub struct CollabPanel {
subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>,
collapsed_channels: Vec<ChannelId>,
// drag_target_channel: ChannelDragTarget,
drag_target_channel: ChannelDragTarget,
workspace: WeakView<Workspace>,
// context_menu_on_selected: bool,
}
// #[derive(PartialEq, Eq)]
// enum ChannelDragTarget {
// None,
// Root,
// Channel(ChannelId),
// }
#[derive(PartialEq, Eq)]
enum ChannelDragTarget {
None,
Root,
Channel(ChannelId),
}
#[derive(Serialize, Deserialize)]
struct SerializedCollabPanel {
@ -614,7 +614,7 @@ impl CollabPanel {
workspace: workspace.weak_handle(),
client: workspace.app_state().client.clone(),
// context_menu_on_selected: true,
// drag_target_channel: ChannelDragTarget::None,
drag_target_channel: ChannelDragTarget::None,
// list_state,
};
@ -2233,20 +2233,20 @@ impl CollabPanel {
// self.toggle_channel_collapsed(action.location, cx);
// }
// fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
// 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 toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
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); todo!()
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()
@ -2346,11 +2346,12 @@ impl CollabPanel {
// }
// }
// fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
// if let Some(workspace) = self.workspace.upgrade(cx) {
// ChannelView::open(action.channel_id, workspace, cx).detach();
// }
// }
fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade() {
todo!();
// ChannelView::open(action.channel_id, workspace, cx).detach();
}
}
// fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
// let Some(channel) = self.selected_channel() else {
@ -2504,21 +2505,22 @@ impl CollabPanel {
// .detach_and_log_err(cx);
// }
// fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
// let Some(workspace) = self.workspace.upgrade(cx) else {
// return;
// };
// let Some(handle) = cx.window().downcast::<Workspace>() else {
// return;
// };
// workspace::join_channel(
// channel_id,
// workspace.read(cx).app_state().clone(),
// Some(handle),
// cx,
// )
// .detach_and_log_err(cx)
// }
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
return;
};
let active_call = ActiveCall::global(cx);
cx.spawn(|_, mut cx| async move {
active_call
.update(&mut cx, |active_call, cx| {
active_call.join_channel(channel_id, Some(handle), cx)
})
.log_err()?
.await
.notify_async_err(&mut cx)
})
.detach()
}
// fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
// let channel_id = action.channel_id;
@ -2982,9 +2984,7 @@ impl CollabPanel {
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
ListItem::new("contact-placeholder")
.child(Label::new("Add a Contact"))
.on_click(cx.listener(|this, _, cx| todo!()))
ListItem::new("contact-placeholder").child(Label::new("Add a Contact"))
// enum AddContacts {}
// MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
// let style = theme.list_empty_state.style_for(is_selected, state);
@ -3023,6 +3023,15 @@ impl CollabPanel {
) -> 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)
@ -3034,17 +3043,7 @@ impl CollabPanel {
.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok())
.unwrap_or(false);
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 has_messages_notification = channel.unseen_message_id.is_some() || true;
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;
@ -3052,6 +3051,7 @@ impl CollabPanel {
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
@ -3059,6 +3059,7 @@ impl CollabPanel {
.filter_map(|user| Some(Avatar::data(user.avatar.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
@ -3081,7 +3082,7 @@ impl CollabPanel {
.w_full()
.justify_between()
.child(
div()
h_stack()
.id(channel_id as usize)
.child(Label::new(channel.name.clone()))
.children(face_pile.map(|face_pile| face_pile.render(cx)))
@ -3092,11 +3093,10 @@ impl CollabPanel {
.child(
div()
.id("channel_chat")
.bg(gpui::blue())
.when(!has_messages_notification, |el| el.invisible())
.group_hover("", |style| style.visible())
.child(
IconButton::new("test_chat", Icon::MessageBubbles)
IconButton::new("channel_chat", Icon::MessageBubbles)
.color(if has_messages_notification {
Color::Default
} else {
@ -3111,20 +3111,16 @@ impl CollabPanel {
.when(!has_notes_notification, |el| el.invisible())
.group_hover("", |style| style.visible())
.child(
div().child("Notes").id("test_notes").tooltip(|cx| {
Tooltip::text("Open channel notes", cx)
}),
), // .child(
// IconButton::new("channel_notes", Icon::File)
// .color(if has_notes_notification {
// Color::Default
// } else {
// Color::Muted
// })
// .tooltip(|cx| {
// Tooltip::text("Open channel notes", cx)
// }),
// ),
IconButton::new("channel_notes", Icon::File)
.color(if has_notes_notification {
Color::Default
} else {
Color::Muted
})
.tooltip(|cx| {
Tooltip::text("Open channel notes", cx)
}),
),
),
),
)
@ -3133,7 +3129,18 @@ impl CollabPanel {
} else {
Toggle::NotToggleable
})
.on_click(cx.listener(|this, _, cx| todo!()))
.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(&OpenChannelNotes { channel_id }, cx)
} else {
this.join_channel(channel_id, cx)
}
}
}))
.on_secondary_mouse_down(cx.listener(|this, _, cx| {
todo!() // open context menu
})),

View File

@ -1,19 +1,30 @@
use gpui::{div, Element, ParentElement};
use std::rc::Rc;
use crate::{Color, Icon, IconElement, IconSize, Toggle};
use gpui::{div, Element, IntoElement, MouseDownEvent, ParentElement, WindowContext};
pub fn disclosure_control(toggle: Toggle) -> impl Element {
use crate::{Color, Icon, IconButton, IconSize, Toggle};
pub fn disclosure_control(
toggle: Toggle,
on_toggle: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
) -> impl Element {
match (toggle.is_toggleable(), toggle.is_toggled()) {
(false, _) => div(),
(_, true) => div().child(
IconElement::new(Icon::ChevronDown)
IconButton::new("toggle", Icon::ChevronDown)
.color(Color::Muted)
.size(IconSize::Small),
.size(IconSize::Small)
.when_some(on_toggle, move |el, on_toggle| {
el.on_click(move |e, cx| on_toggle(e, cx))
}),
),
(_, false) => div().child(
IconElement::new(Icon::ChevronRight)
IconButton::new("toggle", Icon::ChevronRight)
.color(Color::Muted)
.size(IconSize::Small),
.size(IconSize::Small)
.when_some(on_toggle, move |el, on_toggle| {
el.on_click(move |e, cx| on_toggle(e, cx))
}),
),
}
}

View File

@ -1,4 +1,4 @@
use crate::{h_stack, prelude::*, Icon, IconElement};
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
#[derive(IntoElement)]
@ -6,6 +6,7 @@ pub struct IconButton {
id: ElementId,
icon: Icon,
color: Color,
size: IconSize,
variant: ButtonVariant,
state: InteractionState,
selected: bool,
@ -50,7 +51,11 @@ impl RenderOnce for IconButton {
// place we use an icon button.
// .hover(|style| style.bg(bg_hover_color))
.active(|style| style.bg(bg_active_color))
.child(IconElement::new(self.icon).color(icon_color));
.child(
IconElement::new(self.icon)
.size(self.size)
.color(icon_color),
);
if let Some(click_handler) = self.on_mouse_down {
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
@ -76,6 +81,7 @@ impl IconButton {
id: id.into(),
icon,
color: Color::default(),
size: Default::default(),
variant: ButtonVariant::default(),
state: InteractionState::default(),
selected: false,
@ -94,6 +100,11 @@ impl IconButton {
self
}
pub fn size(mut self, size: IconSize) -> Self {
self.size = size;
self
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self

View File

@ -63,7 +63,7 @@ impl RenderOnce for ListHeader {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let disclosure_control = disclosure_control(self.toggle);
let disclosure_control = disclosure_control(self.toggle, None);
let meta = match self.meta {
Some(ListHeaderMeta::Tools(icons)) => div().child(
@ -177,6 +177,7 @@ pub struct ListItem {
toggle: Toggle,
inset: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
on_toggle: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
children: SmallVec<[AnyElement; 2]>,
}
@ -193,6 +194,7 @@ impl ListItem {
inset: false,
on_click: None,
on_secondary_mouse_down: None,
on_toggle: None,
children: SmallVec::new(),
}
}
@ -230,6 +232,14 @@ impl ListItem {
self
}
pub fn on_toggle(
mut self,
on_toggle: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.on_toggle = Some(Rc::new(on_toggle));
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
@ -283,7 +293,7 @@ impl RenderOnce for ListItem {
this.bg(cx.theme().colors().ghost_element_selected)
})
.when_some(self.on_click.clone(), |this, on_click| {
this.on_click(move |event, cx| {
this.cursor_pointer().on_click(move |event, cx| {
// HACK: GPUI currently fires `on_click` with any mouse button,
// but we only care about the left button.
if event.down.button == MouseButton::Left {
@ -304,7 +314,7 @@ impl RenderOnce for ListItem {
.gap_1()
.items_center()
.relative()
.child(disclosure_control(self.toggle))
.child(disclosure_control(self.toggle, self.on_toggle))
.children(left_content)
.children(self.children)
// HACK: We need to attach the `on_click` handler to the child element in order to have the click