;
+
+ fn render(&mut self, cx: &mut ViewContext
) -> Self::Element {
+ let content = self.content_to_render(cx);
+
+ let mut result = h_stack()
+ .id("activity-indicator")
+ .on_action(cx.listener(Self::show_error_message))
+ .on_action(cx.listener(Self::dismiss_error_message));
+
+ if let Some(on_click) = content.on_click {
+ result = result
+ .cursor(CursorStyle::PointingHand)
+ .on_click(cx.listener(move |this, _, cx| {
+ on_click(this, cx);
+ }))
+ }
+
+ result
+ .children(content.icon.map(|icon| svg().path(icon)))
+ .child(SharedString::from(content.message))
+ }
+}
+
+impl StatusItemView for ActivityIndicator {
+ fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {}
+}
diff --git a/crates/auto_update2/src/auto_update.rs b/crates/auto_update2/src/auto_update.rs
index aeff68965fd07ce7eda4cc0aac9bb8a7aaeb4649..d2eab15d09967a84c5c11004ec70419795e0bf01 100644
--- a/crates/auto_update2/src/auto_update.rs
+++ b/crates/auto_update2/src/auto_update.rs
@@ -84,8 +84,8 @@ impl Settings for AutoUpdateSetting {
pub fn init(http_client: Arc, server_url: String, cx: &mut AppContext) {
AutoUpdateSetting::register(cx);
- cx.observe_new_views(|wokrspace: &mut Workspace, _cx| {
- wokrspace
+ cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+ workspace
.register_action(|_, action: &Check, cx| check(action, cx))
.register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
@@ -94,6 +94,11 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo
})
.detach();
});
+
+ // @nate - code to trigger update notification on launch
+ // workspace.show_notification(0, _cx, |cx| {
+ // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
+ // });
})
.detach();
@@ -131,7 +136,7 @@ pub fn check(_: &Check, cx: &mut AppContext) {
}
}
-fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
+pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) {
let auto_updater = auto_updater.read(cx);
let server_url = &auto_updater.server_url;
diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs
index e6a22b73248a8fce898c6871abb06d602a3a8e7a..4a2efcf8076bb882a67c1f25b6300dd4cd48c1d1 100644
--- a/crates/auto_update2/src/update_notification.rs
+++ b/crates/auto_update2/src/update_notification.rs
@@ -1,87 +1,56 @@
-use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext};
-use menu::Cancel;
-use workspace::notifications::NotificationEvent;
+use gpui::{
+ div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
+ SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
+};
+use util::channel::ReleaseChannel;
+use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
pub struct UpdateNotification {
- _version: SemanticVersion,
+ version: SemanticVersion,
}
-impl EventEmitter for UpdateNotification {}
+impl EventEmitter for UpdateNotification {}
impl Render for UpdateNotification {
type Element = Div;
- fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element {
- div().child("Updated zed!")
- // let theme = theme::current(cx).clone();
- // let theme = &theme.update_notification;
-
- // let app_name = cx.global::().display_name();
-
- // MouseEventHandler::new::(0, cx, |state, cx| {
- // Flex::column()
- // .with_child(
- // Flex::row()
- // .with_child(
- // Text::new(
- // format!("Updated to {app_name} {}", self.version),
- // theme.message.text.clone(),
- // )
- // .contained()
- // .with_style(theme.message.container)
- // .aligned()
- // .top()
- // .left()
- // .flex(1., true),
- // )
- // .with_child(
- // MouseEventHandler::new::(0, cx, |state, _| {
- // let style = theme.dismiss_button.style_for(state);
- // Svg::new("icons/x.svg")
- // .with_color(style.color)
- // .constrained()
- // .with_width(style.icon_width)
- // .aligned()
- // .contained()
- // .with_style(style.container)
- // .constrained()
- // .with_width(style.button_width)
- // .with_height(style.button_width)
- // })
- // .with_padding(Padding::uniform(5.))
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // this.dismiss(&Default::default(), cx)
- // })
- // .aligned()
- // .constrained()
- // .with_height(cx.font_cache().line_height(theme.message.text.font_size))
- // .aligned()
- // .top()
- // .flex_float(),
- // ),
- // )
- // .with_child({
- // let style = theme.action_message.style_for(state);
- // Text::new("View the release notes", style.text.clone())
- // .contained()
- // .with_style(style.container)
- // })
- // .contained()
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .on_click(MouseButton::Left, |_, _, cx| {
- // crate::view_release_notes(&Default::default(), cx)
- // })
- // .into_any_named("update notification")
+ fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element {
+ let app_name = cx.global::().display_name();
+
+ v_stack()
+ .elevation_3(cx)
+ .p_4()
+ .child(
+ h_stack()
+ .justify_between()
+ .child(Label::new(format!(
+ "Updated to {app_name} {}",
+ self.version
+ )))
+ .child(
+ div()
+ .id("cancel")
+ .child(IconElement::new(Icon::Close))
+ .cursor_pointer()
+ .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
+ ),
+ )
+ .child(
+ div()
+ .id("notes")
+ .child(Label::new("View the release notes"))
+ .cursor_pointer()
+ .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)),
+ )
}
}
impl UpdateNotification {
pub fn new(version: SemanticVersion) -> Self {
- Self { _version: version }
+ Self { version }
}
- pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) {
- cx.emit(NotificationEvent::Dismiss);
+ pub fn dismiss(&mut self, cx: &mut ViewContext) {
+ cx.emit(DismissEvent);
}
}
diff --git a/crates/call2/Cargo.toml b/crates/call2/Cargo.toml
index 43e19b4ccb4738c42654dffaf7797d75dc88c084..8dc37f68dd7bd9b91c1d1fab240448eb25119cbf 100644
--- a/crates/call2/Cargo.toml
+++ b/crates/call2/Cargo.toml
@@ -31,16 +31,19 @@ media = { path = "../media" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" }
+ui = {package = "ui2", path = "../ui2"}
workspace = {package = "workspace2", path = "../workspace2"}
async-trait.workspace = true
anyhow.workspace = true
async-broadcast = "0.4"
futures.workspace = true
+image = "0.23"
postage.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_derive.workspace = true
+smallvec.workspace = true
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs
index 8e553a9b166ba34bfd6f85070f2b062070315a35..a93305772312cab3624f995e0dd49751554867e1 100644
--- a/crates/call2/src/call2.rs
+++ b/crates/call2/src/call2.rs
@@ -1,8 +1,9 @@
pub mod call_settings;
pub mod participant;
pub mod room;
+mod shared_screen;
-use anyhow::{anyhow, bail, Result};
+use anyhow::{anyhow, Result};
use async_trait::async_trait;
use audio::Audio;
use call_settings::CallSettings;
@@ -13,8 +14,8 @@ use client::{
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
- AppContext, AsyncAppContext, AsyncWindowContext, Context, EventEmitter, Model, ModelContext,
- Subscription, Task, View, ViewContext, WeakModel, WeakView,
+ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
+ Subscription, Task, View, ViewContext, VisualContext, WeakModel, WindowHandle,
};
pub use participant::ParticipantLocation;
use postage::watch;
@@ -22,6 +23,7 @@ use project::Project;
use room::Event;
pub use room::Room;
use settings::Settings;
+use shared_screen::SharedScreen;
use std::sync::Arc;
use util::ResultExt;
use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
@@ -332,12 +334,55 @@ impl ActiveCall {
pub fn join_channel(
&mut self,
channel_id: u64,
+ requesting_window: Option>,
cx: &mut ModelContext,
) -> Task>>> {
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));
}
}
@@ -512,24 +557,17 @@ pub fn report_call_event_for_channel(
pub struct Call {
active_call: Option<(Model, Vec)>,
- parent_workspace: WeakView,
}
impl Call {
- pub fn new(
- parent_workspace: WeakView,
- cx: &mut ViewContext<'_, Workspace>,
- ) -> Box {
+ pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box {
let mut active_call = None;
if cx.has_global::>() {
let call = cx.global::>().clone();
let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
active_call = Some((call, subscriptions));
}
- Box::new(Self {
- active_call,
- parent_workspace,
- })
+ Box::new(Self { active_call })
}
fn on_active_call_event(
workspace: &mut Workspace,
@@ -549,45 +587,10 @@ impl Call {
#[async_trait(?Send)]
impl CallHandler for Call {
- fn shared_screen_for_peer(
- &self,
- peer_id: PeerId,
- _pane: &View,
- cx: &mut ViewContext,
- ) -> Option> {
- let (call, _) = self.active_call.as_ref()?;
- let room = call.read(cx).room()?.read(cx);
- let participant = room.remote_participant_for_peer_id(peer_id)?;
- let _track = participant.video_tracks.values().next()?.clone();
- let _user = participant.user.clone();
- todo!();
- // for item in pane.read(cx).items_of_type::() {
- // if item.read(cx).peer_id == peer_id {
- // return Box::new(Some(item));
- // }
- // }
-
- // Some(Box::new(cx.build_view(|cx| {
- // SharedScreen::new(&track, peer_id, user.clone(), cx)
- // })))
- }
-
- fn room_id(&self, cx: &AppContext) -> Option {
- Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
- }
- fn hang_up(&self, mut cx: AsyncWindowContext) -> Result>> {
- let Some((call, _)) = self.active_call.as_ref() else {
- bail!("Cannot exit a call; not in a call");
- };
-
- call.update(&mut cx, |this, cx| this.hang_up(cx))
- }
- fn active_project(&self, cx: &AppContext) -> Option> {
- ActiveCall::global(cx).read(cx).location().cloned()
- }
fn peer_state(
&mut self,
leader_id: PeerId,
+ project: &Model,
cx: &mut ViewContext,
) -> Option<(bool, bool)> {
let (call, _) = self.active_call.as_ref()?;
@@ -599,12 +602,7 @@ impl CallHandler for Call {
match participant.location {
ParticipantLocation::SharedProject { project_id } => {
leader_in_this_app = true;
- leader_in_this_project = Some(project_id)
- == self
- .parent_workspace
- .update(cx, |this, cx| this.project().read(cx).remote_id())
- .log_err()
- .flatten();
+ leader_in_this_project = Some(project_id) == project.read(cx).remote_id();
}
ParticipantLocation::UnsharedProject => {
leader_in_this_app = true;
@@ -618,6 +616,134 @@ impl CallHandler for Call {
Some((leader_in_this_project, leader_in_this_app))
}
+
+ fn shared_screen_for_peer(
+ &self,
+ peer_id: PeerId,
+ pane: &View,
+ cx: &mut ViewContext,
+ ) -> Option> {
+ let (call, _) = self.active_call.as_ref()?;
+ let room = call.read(cx).room()?.read(cx);
+ let participant = room.remote_participant_for_peer_id(peer_id)?;
+ let track = participant.video_tracks.values().next()?.clone();
+ let user = participant.user.clone();
+ for item in pane.read(cx).items_of_type::() {
+ if item.read(cx).peer_id == peer_id {
+ return Some(Box::new(item));
+ }
+ }
+
+ Some(Box::new(cx.build_view(|cx| {
+ SharedScreen::new(&track, peer_id, user.clone(), cx)
+ })))
+ }
+ fn room_id(&self, cx: &AppContext) -> Option {
+ Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
+ }
+ fn hang_up(&self, cx: &mut AppContext) -> Task> {
+ let Some((call, _)) = self.active_call.as_ref() else {
+ return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
+ };
+
+ call.update(cx, |this, cx| this.hang_up(cx))
+ }
+ fn active_project(&self, cx: &AppContext) -> Option> {
+ ActiveCall::global(cx).read(cx).location().cloned()
+ }
+ fn invite(
+ &mut self,
+ called_user_id: u64,
+ initial_project: Option>,
+ cx: &mut AppContext,
+ ) -> Task> {
+ ActiveCall::global(cx).update(cx, |this, cx| {
+ this.invite(called_user_id, initial_project, cx)
+ })
+ }
+ fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>> {
+ self.active_call
+ .as_ref()
+ .map(|call| {
+ call.0.read(cx).room().map(|room| {
+ room.read(cx)
+ .remote_participants()
+ .iter()
+ .map(|participant| {
+ (participant.1.user.clone(), participant.1.peer_id.clone())
+ })
+ .collect()
+ })
+ })
+ .flatten()
+ }
+ fn is_muted(&self, cx: &AppContext) -> Option {
+ self.active_call
+ .as_ref()
+ .map(|call| {
+ call.0
+ .read(cx)
+ .room()
+ .map(|room| room.read(cx).is_muted(cx))
+ })
+ .flatten()
+ }
+ fn toggle_mute(&self, cx: &mut AppContext) {
+ self.active_call.as_ref().map(|call| {
+ call.0.update(cx, |this, cx| {
+ this.room().map(|room| {
+ let room = room.clone();
+ cx.spawn(|_, mut cx| async move {
+ room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
+ .await
+ })
+ .detach_and_log_err(cx);
+ })
+ })
+ });
+ }
+ fn toggle_screen_share(&self, cx: &mut AppContext) {
+ self.active_call.as_ref().map(|call| {
+ call.0.update(cx, |this, cx| {
+ this.room().map(|room| {
+ room.update(cx, |this, cx| {
+ if this.is_screen_sharing() {
+ this.unshare_screen(cx).log_err();
+ } else {
+ let t = this.share_screen(cx);
+ cx.spawn(move |_, _| async move {
+ t.await.log_err();
+ })
+ .detach();
+ }
+ })
+ })
+ })
+ });
+ }
+ fn toggle_deafen(&self, cx: &mut AppContext) {
+ self.active_call.as_ref().map(|call| {
+ call.0.update(cx, |this, cx| {
+ this.room().map(|room| {
+ room.update(cx, |this, cx| {
+ this.toggle_deafen(cx).log_err();
+ })
+ })
+ })
+ });
+ }
+ fn is_deafened(&self, cx: &AppContext) -> Option {
+ self.active_call
+ .as_ref()
+ .map(|call| {
+ call.0
+ .read(cx)
+ .room()
+ .map(|room| room.read(cx).is_deafened())
+ })
+ .flatten()
+ .flatten()
+ }
}
#[cfg(test)]
diff --git a/crates/call2/src/participant.rs b/crates/call2/src/participant.rs
index f62d103f1758d85f6e4a12cc10c1797c7cd80f50..325a4f812b2f58c1b1bb0cc56f042e891df435d4 100644
--- a/crates/call2/src/participant.rs
+++ b/crates/call2/src/participant.rs
@@ -4,7 +4,7 @@ use client::{proto, User};
use collections::HashMap;
use gpui::WeakModel;
pub use live_kit_client::Frame;
-use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
+pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
use project::Project;
use std::sync::Arc;
diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs
index 87118764fdc717620521b32e5b77aa7e1c4aca9f..694966abe9d509f5dc9cad5353a63aac074eaf50 100644
--- a/crates/call2/src/room.rs
+++ b/crates/call2/src/room.rs
@@ -21,7 +21,7 @@ use live_kit_client::{
};
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
-use settings::Settings;
+use settings::Settings as _;
use std::{future::Future, mem, sync::Arc, time::Duration};
use util::{post_inc, ResultExt, TryFutureExt};
@@ -1267,7 +1267,6 @@ impl Room {
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
.await
};
-
let publication = publish_track.await;
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
diff --git a/crates/call2/src/shared_screen.rs b/crates/call2/src/shared_screen.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c38ebeac021d59c810fc27ff528ddc773f9642f4
--- /dev/null
+++ b/crates/call2/src/shared_screen.rs
@@ -0,0 +1,111 @@
+use crate::participant::{Frame, RemoteVideoTrack};
+use anyhow::Result;
+use client::{proto::PeerId, User};
+use futures::StreamExt;
+use gpui::{
+ div, img, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable, FocusableView,
+ InteractiveElement, ParentElement, Render, SharedString, Styled, Task, View, ViewContext,
+ VisualContext, WindowContext,
+};
+use std::sync::{Arc, Weak};
+use ui::{h_stack, Icon, IconElement};
+use workspace::{item::Item, ItemNavHistory, WorkspaceId};
+
+pub enum Event {
+ Close,
+}
+
+pub struct SharedScreen {
+ track: Weak,
+ frame: Option,
+ pub peer_id: PeerId,
+ user: Arc,
+ nav_history: Option,
+ _maintain_frame: Task>,
+ focus: FocusHandle,
+}
+
+impl SharedScreen {
+ pub fn new(
+ track: &Arc,
+ peer_id: PeerId,
+ user: Arc,
+ cx: &mut ViewContext,
+ ) -> Self {
+ cx.focus_handle();
+ let mut frames = track.frames();
+ Self {
+ track: Arc::downgrade(track),
+ frame: None,
+ peer_id,
+ user,
+ nav_history: Default::default(),
+ _maintain_frame: cx.spawn(|this, mut cx| async move {
+ while let Some(frame) = frames.next().await {
+ this.update(&mut cx, |this, cx| {
+ this.frame = Some(frame);
+ cx.notify();
+ })?;
+ }
+ this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
+ Ok(())
+ }),
+ focus: cx.focus_handle(),
+ }
+ }
+}
+
+impl EventEmitter for SharedScreen {}
+impl EventEmitter for SharedScreen {}
+
+impl FocusableView for SharedScreen {
+ fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+ self.focus.clone()
+ }
+}
+impl Render for SharedScreen {
+ type Element = Focusable;
+
+ fn render(&mut self, _: &mut ViewContext
) -> Self::Element {
+ div().track_focus(&self.focus).size_full().children(
+ self.frame
+ .as_ref()
+ .map(|frame| img(frame.image()).size_full()),
+ )
+ }
+}
+
+impl Item for SharedScreen {
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option {
+ Some(format!("{}'s screen", self.user.github_login).into())
+ }
+ fn deactivated(&mut self, cx: &mut ViewContext) {
+ if let Some(nav_history) = self.nav_history.as_mut() {
+ nav_history.push::<()>(None, cx);
+ }
+ }
+
+ fn tab_content(&self, _: Option, _: &WindowContext<'_>) -> gpui::AnyElement {
+ h_stack()
+ .gap_1()
+ .child(IconElement::new(Icon::Screen))
+ .child(SharedString::from(format!(
+ "{}'s screen",
+ self.user.github_login
+ )))
+ .into_any()
+ }
+
+ fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) {
+ self.nav_history = Some(history);
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ cx: &mut ViewContext,
+ ) -> Option> {
+ let track = self.track.upgrade()?;
+ Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
+ }
+}
diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs
index 4ad354f2f91bd56cdb0c1f657137d1beac9a3e4f..4746c9c6e4222a9c08383001525cf0f9cc55ab09 100644
--- a/crates/client2/src/client2.rs
+++ b/crates/client2/src/client2.rs
@@ -551,7 +551,6 @@ impl Client {
F: 'static + Future