Detailed changes
@@ -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));
}
}
@@ -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();
@@ -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)
@@ -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
})),
@@ -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))
+ }),
),
}
}
@@ -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
@@ -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