diff --git a/Cargo.lock b/Cargo.lock
index a0be9756bfd4da7d747c07300a62bc5fb0ad226c..eb210eb797ec972d3446f5f47eaa55de6df5b922 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1552,6 +1552,7 @@ dependencies = [
"clock",
"collections",
"context_menu",
+ "db",
"editor",
"feedback",
"futures 0.3.28",
@@ -1563,9 +1564,11 @@ dependencies = [
"postage",
"project",
"recent_projects",
+ "schemars",
"serde",
"serde_derive",
"settings",
+ "staff_mode",
"theme",
"theme_selector",
"util",
diff --git a/assets/icons/ai.svg b/assets/icons/ai.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5b3faaa9ccd8683d868173d783d45a9ef111d7cd
--- /dev/null
+++ b/assets/icons/ai.svg
@@ -0,0 +1,23 @@
+
diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg
new file mode 100644
index 0000000000000000000000000000000000000000..186c9c7457c48405508de337fa5d1904f2563f59
--- /dev/null
+++ b/assets/icons/arrow_left.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7bae7f4801a10b0ee04dfab93048bbdaf526045a
--- /dev/null
+++ b/assets/icons/arrow_right.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/channel_hash.svg b/assets/icons/channel_hash.svg
new file mode 100644
index 0000000000000000000000000000000000000000..edd04626782e52bc2f3c1a73a08f2de166828c33
--- /dev/null
+++ b/assets/icons/channel_hash.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/check.svg b/assets/icons/check.svg
new file mode 100644
index 0000000000000000000000000000000000000000..77b180892cfaf3b230cee3b4d9303a0fefac6e06
--- /dev/null
+++ b/assets/icons/check.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg
new file mode 100644
index 0000000000000000000000000000000000000000..85ba2e1f37724edc819c58bbdf3009ca491f760d
--- /dev/null
+++ b/assets/icons/check_circle.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b971555cfa0b8c15daf35522a3f3ef449ffac087
--- /dev/null
+++ b/assets/icons/chevron_down.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/chevron_left.svg b/assets/icons/chevron_left.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8e61beed5df055132edde2510908324cc8a47fb1
--- /dev/null
+++ b/assets/icons/chevron_left.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fcd9d83fc203578f5135a5d040999bea6765769e
--- /dev/null
+++ b/assets/icons/chevron_right.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/chevron_up.svg b/assets/icons/chevron_up.svg
new file mode 100644
index 0000000000000000000000000000000000000000..171cdd61c0511aabe2f25463089d3cfd9cbf5039
--- /dev/null
+++ b/assets/icons/chevron_up.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/conversations.svg b/assets/icons/conversations.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fe8ad03dda712b2b48d75b480e95ca534c1efb9c
--- /dev/null
+++ b/assets/icons/conversations.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/copilot.svg b/assets/icons/copilot.svg
new file mode 100644
index 0000000000000000000000000000000000000000..06dbf178ae9727569eaa9b6f9bbe986a3d488e48
--- /dev/null
+++ b/assets/icons/copilot.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4aa44979c39de058a96548d66a73fe6b437f22eb
--- /dev/null
+++ b/assets/icons/copy.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1858c655202cf6940c90278b43241bb1cabc32ac
--- /dev/null
+++ b/assets/icons/ellipsis.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/error.svg b/assets/icons/error.svg
new file mode 100644
index 0000000000000000000000000000000000000000..82b9401d08dc8d682fcbbfda15795f6ec3d3de2e
--- /dev/null
+++ b/assets/icons/error.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7e45535773e4e6f871fd80af25452afb5021fdd4
--- /dev/null
+++ b/assets/icons/exit.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/feedback.svg b/assets/icons/feedback.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2703f7011994869bcad75a7a0f45f1b8e89317af
--- /dev/null
+++ b/assets/icons/feedback.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg
new file mode 100644
index 0000000000000000000000000000000000000000..80ce656f57199246dc036f39e2fead4e19e53168
--- /dev/null
+++ b/assets/icons/filter.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/hash.svg b/assets/icons/hash.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f685245ed3c2a2881b2fe1377bf029e8e515ae94
--- /dev/null
+++ b/assets/icons/hash.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/html.svg b/assets/icons/html.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1e676fe313401fc137813827df03cc2c60851df0
--- /dev/null
+++ b/assets/icons/html.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/kebab.svg b/assets/icons/kebab.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1858c655202cf6940c90278b43241bb1cabc32ac
--- /dev/null
+++ b/assets/icons/kebab.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg
new file mode 100644
index 0000000000000000000000000000000000000000..652f45a7e843795c288fdaaf4951d40943e3805d
--- /dev/null
+++ b/assets/icons/lock.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/magnifying_glass.svg b/assets/icons/magnifying_glass.svg
new file mode 100644
index 0000000000000000000000000000000000000000..0b539adb6c764451234b898a5e6306baabc64d57
--- /dev/null
+++ b/assets/icons/magnifying_glass.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/match_case.svg b/assets/icons/match_case.svg
new file mode 100644
index 0000000000000000000000000000000000000000..82f4529c1b054d4218812f7b8a2094f54e9a1ae3
--- /dev/null
+++ b/assets/icons/match_case.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/match_word.svg b/assets/icons/match_word.svg
new file mode 100644
index 0000000000000000000000000000000000000000..69ba8eb9e6bc52e49e4ace4b1526881222672d6c
--- /dev/null
+++ b/assets/icons/match_word.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4dc7755714990ddc5d4b06ffc992859954342c93
--- /dev/null
+++ b/assets/icons/maximize.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/microphone.svg b/assets/icons/microphone.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8974fd939d233b839d03e94e301abb2a955c665a
--- /dev/null
+++ b/assets/icons/microphone.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d8941ee1f0ed6a566cf0d07a1b89cefd49d3ee19
--- /dev/null
+++ b/assets/icons/minimize.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a54dd0ad66226f3c485c33c221f823da87727789
--- /dev/null
+++ b/assets/icons/plus.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/project.svg b/assets/icons/project.svg
new file mode 100644
index 0000000000000000000000000000000000000000..525109db4ce74d99074c90e714003720d4e97156
--- /dev/null
+++ b/assets/icons/project.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/replace.svg b/assets/icons/replace.svg
new file mode 100644
index 0000000000000000000000000000000000000000..af1092189130fea8cfc52a54b2fbc1e71636b1fd
--- /dev/null
+++ b/assets/icons/replace.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/icons/replace_all.svg b/assets/icons/replace_all.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4838e82242f38d41357cf189f761a43535ff51f0
--- /dev/null
+++ b/assets/icons/replace_all.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/replace_next.svg b/assets/icons/replace_next.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ba751411afc33e6fd79c005fd0f06f2250dd4276
--- /dev/null
+++ b/assets/icons/replace_next.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/screen.svg b/assets/icons/screen.svg
new file mode 100644
index 0000000000000000000000000000000000000000..49e097b02325ce3644be662896cd7a3a666b6f8f
--- /dev/null
+++ b/assets/icons/screen.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/split.svg b/assets/icons/split.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4c131466c2e2dbb0752f3e6eebbe2b92775550df
--- /dev/null
+++ b/assets/icons/split.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/success.svg b/assets/icons/success.svg
new file mode 100644
index 0000000000000000000000000000000000000000..85450cdc433b80f157be94beae5f60c184906f0f
--- /dev/null
+++ b/assets/icons/success.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/terminal.svg b/assets/icons/terminal.svg
new file mode 100644
index 0000000000000000000000000000000000000000..15dd705b0b313930e1971d24d3774050880b5a4c
--- /dev/null
+++ b/assets/icons/terminal.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6b3d0fd41e979c0704a8f04502c16cfc58c9cb2f
--- /dev/null
+++ b/assets/icons/warning.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/x.svg b/assets/icons/x.svg
new file mode 100644
index 0000000000000000000000000000000000000000..31c5aa31a6b2e90a11249f5dc5c2b4ceb5ffc501
--- /dev/null
+++ b/assets/icons/x.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json
index 38ec8ffb4057d2404c5e38e1150b27bb3acd155d..f4d36ee95bbe666afbff05223c9169274ba1ca72 100644
--- a/assets/keymaps/default.json
+++ b/assets/keymaps/default.json
@@ -13,6 +13,7 @@
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
"enter": "menu::Confirm",
+ "ctrl-enter": "menu::ShowContextMenu",
"cmd-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
@@ -513,7 +514,8 @@
{
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
- "cmd-shift-c": "collab::ToggleContactsMenu",
+ // TODO: Move this to a dock open action
+ "cmd-shift-c": "collab_panel::ToggleFocus",
"cmd-alt-i": "zed::DebugElements"
}
},
@@ -549,6 +551,25 @@
"alt-shift-f": "project_panel::NewSearchInDirectory"
}
},
+ {
+ "context": "CollabPanel",
+ "bindings": {
+ "ctrl-backspace": "collab_panel::Remove",
+ "space": "menu::Confirm"
+ }
+ },
+ {
+ "context": "ChannelModal",
+ "bindings": {
+ "tab": "channel_modal::ToggleMode"
+ }
+ },
+ {
+ "context": "ChannelModal > Picker > Editor",
+ "bindings": {
+ "tab": "channel_modal::ToggleMode"
+ }
+ },
{
"context": "Terminal",
"bindings": {
diff --git a/assets/settings/default.json b/assets/settings/default.json
index c6235e80a1ab370d7ce2ecb9e6072d5e1e1fa330..2ddf4a137fbbc9d7df35fe520923725399bf430f 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -122,7 +122,17 @@
// Amount of indentation for nested items.
"indent_size": 20
},
+ "collaboration_panel": {
+ // Whether to show the collaboration panel button in the status bar.
+ "button": true,
+ // Where to dock channels panel. Can be 'left' or 'right'.
+ "dock": "right",
+ // Default width of the channels panel.
+ "default_width": 240
+ },
"assistant": {
+ // Whether to show the assistant panel button in the status bar.
+ "button": true,
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
"dock": "right",
// Default width when the assistant is docked to the left or right.
diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs
index bb4e9f6db443d7cbdfea6cfa3cb73a0519a68bdc..e0fe41aebee1cbc305422c036ef898f732a2c238 100644
--- a/crates/ai/src/assistant.rs
+++ b/crates/ai/src/assistant.rs
@@ -192,6 +192,7 @@ impl AssistantPanel {
old_dock_position = new_dock_position;
cx.emit(AssistantPanelEvent::DockPositionChanged);
}
+ cx.notify();
})];
this
@@ -780,8 +781,10 @@ impl Panel for AssistantPanel {
}
}
- fn icon_path(&self) -> &'static str {
- "icons/robot_14.svg"
+ fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
+ settings::get::(cx)
+ .button
+ .then(|| "icons/ai.svg")
}
fn icon_tooltip(&self) -> (String, Option>) {
diff --git a/crates/ai/src/assistant_settings.rs b/crates/ai/src/assistant_settings.rs
index eb92e0f6e8c0bdd6e554844f2565057ed92e9ebd..04ba8fb946eb7f450fe95bc7565bb304f8b4a1d7 100644
--- a/crates/ai/src/assistant_settings.rs
+++ b/crates/ai/src/assistant_settings.rs
@@ -13,6 +13,7 @@ pub enum AssistantDockPosition {
#[derive(Deserialize, Debug)]
pub struct AssistantSettings {
+ pub button: bool,
pub dock: AssistantDockPosition,
pub default_width: f32,
pub default_height: f32,
@@ -20,6 +21,7 @@ pub struct AssistantSettings {
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContent {
+ pub button: Option,
pub dock: Option,
pub default_width: Option,
pub default_height: Option,
diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs
index 233b0f62aa5aa02d82e1524a2c7ea0fa96b836ce..d80fb6738f69891a9199f230af832ee335071496 100644
--- a/crates/audio/src/audio.rs
+++ b/crates/audio/src/audio.rs
@@ -39,29 +39,43 @@ pub struct Audio {
impl Audio {
pub fn new() -> Self {
- let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
-
Self {
- _output_stream,
- output_handle,
+ _output_stream: None,
+ output_handle: None,
}
}
- pub fn play_sound(sound: Sound, cx: &AppContext) {
+ fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
+ if self.output_handle.is_none() {
+ let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
+ self.output_handle = output_handle;
+ self._output_stream = _output_stream;
+ }
+
+ self.output_handle.as_ref()
+ }
+
+ pub fn play_sound(sound: Sound, cx: &mut AppContext) {
if !cx.has_global::() {
return;
}
- let this = cx.global::();
+ cx.update_global::(|this, cx| {
+ let output_handle = this.ensure_output_exists()?;
+ let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
+ output_handle.play_raw(source).log_err()?;
+ Some(())
+ });
+ }
- let Some(output_handle) = this.output_handle.as_ref() else {
+ pub fn end_call(cx: &mut AppContext) {
+ if !cx.has_global::() {
return;
- };
-
- let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
- return;
- };
+ }
- output_handle.play_raw(source).log_err();
+ cx.update_global::(|this, _| {
+ this._output_stream.take();
+ this.output_handle.take();
+ });
}
}
diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs
index 2defd6b40f0f778157e6da24684b36d1cd565408..3ac29bfc85af9c47d790168aa2c32f49c14977be 100644
--- a/crates/call/src/call.rs
+++ b/crates/call/src/call.rs
@@ -5,8 +5,11 @@ pub mod room;
use std::sync::Arc;
use anyhow::{anyhow, Result};
+use audio::Audio;
use call_settings::CallSettings;
-use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
+use client::{
+ proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
+};
use collections::HashSet;
use futures::{future::Shared, FutureExt};
use postage::watch;
@@ -75,6 +78,10 @@ impl ActiveCall {
}
}
+ pub fn channel_id(&self, cx: &AppContext) -> Option {
+ self.room()?.read(cx).channel_id()
+ }
+
async fn handle_incoming_call(
this: ModelHandle,
envelope: TypedEnvelope,
@@ -274,9 +281,36 @@ impl ActiveCall {
Ok(())
}
+ pub fn join_channel(
+ &mut self,
+ channel_id: u64,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ if let Some(room) = self.room().cloned() {
+ if room.read(cx).channel_id() == Some(channel_id) {
+ return Task::ready(Ok(()));
+ } else {
+ room.update(cx, |room, cx| room.clear_state(cx));
+ }
+ }
+
+ let join = Room::join_channel(channel_id, self.client.clone(), self.user_store.clone(), cx);
+
+ cx.spawn(|this, mut cx| async move {
+ let room = join.await?;
+ this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
+ .await?;
+ this.update(&mut cx, |this, cx| {
+ this.report_call_event("join channel", cx)
+ });
+ Ok(())
+ })
+ }
+
pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> {
cx.notify();
self.report_call_event("hang up", cx);
+ Audio::end_call(cx);
if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx))
} else {
diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs
index 328a94506c136dad0fbf000b00b391d6c4025b7f..6f01b1d75789ce61d537be2d780f0dbb5960ad17 100644
--- a/crates/call/src/room.rs
+++ b/crates/call/src/room.rs
@@ -49,6 +49,7 @@ pub enum Event {
pub struct Room {
id: u64,
+ channel_id: Option,
live_kit: Option,
status: RoomStatus,
shared_projects: HashSet>,
@@ -93,8 +94,25 @@ impl Entity for Room {
}
impl Room {
+ pub fn channel_id(&self) -> Option {
+ self.channel_id
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn is_connected(&self) -> bool {
+ if let Some(live_kit) = self.live_kit.as_ref() {
+ matches!(
+ *live_kit.room.status().borrow(),
+ live_kit_client::ConnectionState::Connected { .. }
+ )
+ } else {
+ false
+ }
+ }
+
fn new(
id: u64,
+ channel_id: Option,
live_kit_connection_info: Option,
client: Arc,
user_store: ModelHandle,
@@ -185,6 +203,7 @@ impl Room {
Self {
id,
+ channel_id,
live_kit: live_kit_room,
status: RoomStatus::Online,
shared_projects: Default::default(),
@@ -217,6 +236,7 @@ impl Room {
let room = cx.add_model(|cx| {
Self::new(
room_proto.id,
+ None,
response.live_kit_connection_info,
client,
user_store,
@@ -248,35 +268,64 @@ impl Room {
})
}
+ pub(crate) fn join_channel(
+ channel_id: u64,
+ client: Arc,
+ user_store: ModelHandle,
+ cx: &mut AppContext,
+ ) -> Task>> {
+ cx.spawn(|cx| async move {
+ Self::from_join_response(
+ client.request(proto::JoinChannel { channel_id }).await?,
+ client,
+ user_store,
+ cx,
+ )
+ })
+ }
+
pub(crate) fn join(
call: &IncomingCall,
client: Arc,
user_store: ModelHandle,
cx: &mut AppContext,
) -> Task>> {
- let room_id = call.room_id;
- cx.spawn(|mut cx| async move {
- let response = client.request(proto::JoinRoom { id: room_id }).await?;
- let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
- let room = cx.add_model(|cx| {
- Self::new(
- room_id,
- response.live_kit_connection_info,
- client,
- user_store,
- cx,
- )
- });
- room.update(&mut cx, |room, cx| {
- room.leave_when_empty = true;
- room.apply_room_update(room_proto, cx)?;
- anyhow::Ok(())
- })?;
-
- Ok(room)
+ let id = call.room_id;
+ cx.spawn(|cx| async move {
+ Self::from_join_response(
+ client.request(proto::JoinRoom { id }).await?,
+ client,
+ user_store,
+ cx,
+ )
})
}
+ fn from_join_response(
+ response: proto::JoinRoomResponse,
+ client: Arc,
+ user_store: ModelHandle,
+ mut cx: AsyncAppContext,
+ ) -> Result> {
+ let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
+ let room = cx.add_model(|cx| {
+ Self::new(
+ room_proto.id,
+ response.channel_id,
+ response.live_kit_connection_info,
+ client,
+ user_store,
+ cx,
+ )
+ });
+ room.update(&mut cx, |room, cx| {
+ room.leave_when_empty = room.channel_id.is_none();
+ room.apply_room_update(room_proto, cx)?;
+ anyhow::Ok(())
+ })?;
+ Ok(room)
+ }
+
fn should_leave(&self) -> bool {
self.leave_when_empty
&& self.pending_room_update.is_none()
@@ -297,7 +346,18 @@ impl Room {
}
log::info!("leaving room");
+ Audio::play_sound(Sound::Leave, cx);
+
+ self.clear_state(cx);
+
+ let leave_room = self.client.request(proto::LeaveRoom {});
+ cx.background().spawn(async move {
+ leave_room.await?;
+ anyhow::Ok(())
+ })
+ }
+ pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
for project in self.shared_projects.drain() {
if let Some(project) = project.upgrade(cx) {
project.update(cx, |project, cx| {
@@ -314,8 +374,6 @@ impl Room {
}
}
- Audio::play_sound(Sound::Leave, cx);
-
self.status = RoomStatus::Offline;
self.remote_participants.clear();
self.pending_participants.clear();
@@ -324,12 +382,6 @@ impl Room {
self.live_kit.take();
self.pending_room_update.take();
self.maintain_connection.take();
-
- let leave_room = self.client.request(proto::LeaveRoom {});
- cx.background().spawn(async move {
- leave_room.await?;
- anyhow::Ok(())
- })
}
async fn maintain_connection(
@@ -1066,11 +1118,11 @@ impl Room {
})
}
- pub fn is_muted(&self) -> bool {
+ pub fn is_muted(&self, cx: &AppContext) -> bool {
self.live_kit
.as_ref()
.and_then(|live_kit| match &live_kit.microphone_track {
- LocalTrack::None => Some(true),
+ LocalTrack::None => Some(settings::get::(cx).mute_on_join),
LocalTrack::Pending { muted, .. } => Some(*muted),
LocalTrack::Published { muted, .. } => Some(*muted),
})
@@ -1260,7 +1312,7 @@ impl Room {
}
pub fn toggle_mute(&mut self, cx: &mut ModelContext) -> Result>> {
- let should_mute = !self.is_muted();
+ let should_mute = !self.is_muted(cx);
if let Some(live_kit) = self.live_kit.as_mut() {
if matches!(live_kit.microphone_track, LocalTrack::None) {
return Ok(self.share_microphone(cx));
diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e2c18a63a96cf38768c68fc67b856536a53c2d18
--- /dev/null
+++ b/crates/client/src/channel_store.rs
@@ -0,0 +1,548 @@
+use crate::Status;
+use crate::{Client, Subscription, User, UserStore};
+use anyhow::anyhow;
+use anyhow::Result;
+use collections::HashMap;
+use collections::HashSet;
+use futures::channel::mpsc;
+use futures::Future;
+use futures::StreamExt;
+use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
+use rpc::{proto, TypedEnvelope};
+use std::sync::Arc;
+use util::ResultExt;
+
+pub type ChannelId = u64;
+pub type UserId = u64;
+
+pub struct ChannelStore {
+ channels_by_id: HashMap>,
+ channel_paths: Vec>,
+ channel_invitations: Vec>,
+ channel_participants: HashMap>>,
+ channels_with_admin_privileges: HashSet,
+ outgoing_invites: HashSet<(ChannelId, UserId)>,
+ update_channels_tx: mpsc::UnboundedSender,
+ client: Arc,
+ user_store: ModelHandle,
+ _rpc_subscription: Subscription,
+ _watch_connection_status: Task<()>,
+ _update_channels: Task<()>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Channel {
+ pub id: ChannelId,
+ pub name: String,
+}
+
+pub struct ChannelMembership {
+ pub user: Arc,
+ pub kind: proto::channel_member::Kind,
+ pub admin: bool,
+}
+
+pub enum ChannelEvent {
+ ChannelCreated(ChannelId),
+ ChannelRenamed(ChannelId),
+}
+
+impl Entity for ChannelStore {
+ type Event = ChannelEvent;
+}
+
+pub enum ChannelMemberStatus {
+ Invited,
+ Member,
+ NotMember,
+}
+
+impl ChannelStore {
+ pub fn new(
+ client: Arc,
+ user_store: ModelHandle,
+ cx: &mut ModelContext,
+ ) -> Self {
+ let rpc_subscription =
+ client.add_message_handler(cx.handle(), Self::handle_update_channels);
+
+ let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
+ let mut connection_status = client.status();
+ let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
+ while let Some(status) = connection_status.next().await {
+ if matches!(status, Status::ConnectionLost | Status::SignedOut) {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| {
+ this.channels_by_id.clear();
+ this.channel_invitations.clear();
+ this.channel_participants.clear();
+ this.channels_with_admin_privileges.clear();
+ this.channel_paths.clear();
+ this.outgoing_invites.clear();
+ cx.notify();
+ });
+ } else {
+ break;
+ }
+ }
+ }
+ });
+ Self {
+ channels_by_id: HashMap::default(),
+ channel_invitations: Vec::default(),
+ channel_paths: Vec::default(),
+ channel_participants: Default::default(),
+ channels_with_admin_privileges: Default::default(),
+ outgoing_invites: Default::default(),
+ update_channels_tx,
+ client,
+ user_store,
+ _rpc_subscription: rpc_subscription,
+ _watch_connection_status: watch_connection_status,
+ _update_channels: cx.spawn_weak(|this, mut cx| async move {
+ while let Some(update_channels) = update_channels_rx.next().await {
+ if let Some(this) = this.upgrade(&cx) {
+ let update_task = this.update(&mut cx, |this, cx| {
+ this.update_channels(update_channels, cx)
+ });
+ if let Some(update_task) = update_task {
+ update_task.await.log_err();
+ }
+ }
+ }
+ }),
+ }
+ }
+
+ pub fn channel_count(&self) -> usize {
+ self.channel_paths.len()
+ }
+
+ pub fn channels(&self) -> impl '_ + Iterator- )> {
+ self.channel_paths.iter().map(move |path| {
+ let id = path.last().unwrap();
+ let channel = self.channel_for_id(*id).unwrap();
+ (path.len() - 1, channel)
+ })
+ }
+
+ pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc)> {
+ let path = self.channel_paths.get(ix)?;
+ let id = path.last().unwrap();
+ let channel = self.channel_for_id(*id).unwrap();
+ Some((path.len() - 1, channel))
+ }
+
+ pub fn channel_invitations(&self) -> &[Arc] {
+ &self.channel_invitations
+ }
+
+ pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc> {
+ self.channels_by_id.get(&channel_id)
+ }
+
+ pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
+ self.channel_paths.iter().any(|path| {
+ if let Some(ix) = path.iter().position(|id| *id == channel_id) {
+ path[..=ix]
+ .iter()
+ .any(|id| self.channels_with_admin_privileges.contains(id))
+ } else {
+ false
+ }
+ })
+ }
+
+ pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] {
+ self.channel_participants
+ .get(&channel_id)
+ .map_or(&[], |v| v.as_slice())
+ }
+
+ pub fn create_channel(
+ &self,
+ name: &str,
+ parent_id: Option,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ let client = self.client.clone();
+ let name = name.trim_start_matches("#").to_owned();
+ cx.spawn(|this, mut cx| async move {
+ let channel = client
+ .request(proto::CreateChannel { name, parent_id })
+ .await?
+ .channel
+ .ok_or_else(|| anyhow!("missing channel in response"))?;
+
+ let channel_id = channel.id;
+
+ this.update(&mut cx, |this, cx| {
+ let task = this.update_channels(
+ proto::UpdateChannels {
+ channels: vec![channel],
+ ..Default::default()
+ },
+ cx,
+ );
+ assert!(task.is_none());
+
+ // This event is emitted because the collab panel wants to clear the pending edit state
+ // before this frame is rendered. But we can't guarantee that the collab panel's future
+ // will resolve before this flush_effects finishes. Synchronously emitting this event
+ // ensures that the collab panel will observe this creation before the frame completes
+ cx.emit(ChannelEvent::ChannelCreated(channel_id));
+ });
+
+ Ok(channel_id)
+ })
+ }
+
+ pub fn invite_member(
+ &mut self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ admin: bool,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ if !self.outgoing_invites.insert((channel_id, user_id)) {
+ return Task::ready(Err(anyhow!("invite request already in progress")));
+ }
+
+ cx.notify();
+ let client = self.client.clone();
+ cx.spawn(|this, mut cx| async move {
+ let result = client
+ .request(proto::InviteChannelMember {
+ channel_id,
+ user_id,
+ admin,
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.outgoing_invites.remove(&(channel_id, user_id));
+ cx.notify();
+ });
+
+ result?;
+
+ Ok(())
+ })
+ }
+
+ pub fn remove_member(
+ &mut self,
+ channel_id: ChannelId,
+ user_id: u64,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ if !self.outgoing_invites.insert((channel_id, user_id)) {
+ return Task::ready(Err(anyhow!("invite request already in progress")));
+ }
+
+ cx.notify();
+ let client = self.client.clone();
+ cx.spawn(|this, mut cx| async move {
+ let result = client
+ .request(proto::RemoveChannelMember {
+ channel_id,
+ user_id,
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.outgoing_invites.remove(&(channel_id, user_id));
+ cx.notify();
+ });
+ result?;
+ Ok(())
+ })
+ }
+
+ pub fn set_member_admin(
+ &mut self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ admin: bool,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ if !self.outgoing_invites.insert((channel_id, user_id)) {
+ return Task::ready(Err(anyhow!("member request already in progress")));
+ }
+
+ cx.notify();
+ let client = self.client.clone();
+ cx.spawn(|this, mut cx| async move {
+ let result = client
+ .request(proto::SetChannelMemberAdmin {
+ channel_id,
+ user_id,
+ admin,
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.outgoing_invites.remove(&(channel_id, user_id));
+ cx.notify();
+ });
+
+ result?;
+ Ok(())
+ })
+ }
+
+ pub fn rename(
+ &mut self,
+ channel_id: ChannelId,
+ new_name: &str,
+ cx: &mut ModelContext,
+ ) -> Task> {
+ let client = self.client.clone();
+ let name = new_name.to_string();
+ cx.spawn(|this, mut cx| async move {
+ let channel = client
+ .request(proto::RenameChannel { channel_id, name })
+ .await?
+ .channel
+ .ok_or_else(|| anyhow!("missing channel in response"))?;
+ this.update(&mut cx, |this, cx| {
+ let task = this.update_channels(
+ proto::UpdateChannels {
+ channels: vec![channel],
+ ..Default::default()
+ },
+ cx,
+ );
+ assert!(task.is_none());
+
+ // This event is emitted because the collab panel wants to clear the pending edit state
+ // before this frame is rendered. But we can't guarantee that the collab panel's future
+ // will resolve before this flush_effects finishes. Synchronously emitting this event
+ // ensures that the collab panel will observe this creation before the frame complete
+ cx.emit(ChannelEvent::ChannelRenamed(channel_id))
+ });
+ Ok(())
+ })
+ }
+
+ pub fn respond_to_channel_invite(
+ &mut self,
+ channel_id: ChannelId,
+ accept: bool,
+ ) -> impl Future