diff --git a/assets/icons/public.svg b/assets/icons/public.svg
index 55a7968485..38278cdaba 100644
--- a/assets/icons/public.svg
+++ b/assets/icons/public.svg
@@ -1,3 +1,3 @@
diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
index dcb793aa51..8eb6b52fd8 100644
--- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
+++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
@@ -44,7 +44,7 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
CREATE TABLE "projects" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
- "room_id" INTEGER REFERENCES rooms (id) NOT NULL,
+ "room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
diff --git a/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql b/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql
new file mode 100644
index 0000000000..be535ff7fa
--- /dev/null
+++ b/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql
@@ -0,0 +1,8 @@
+-- Add migration script here
+
+ALTER TABLE projects
+ DROP CONSTRAINT projects_room_id_fkey,
+ ADD CONSTRAINT projects_room_id_fkey
+ FOREIGN KEY (room_id)
+ REFERENCES rooms (id)
+ ON DELETE CASCADE;
diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs
index 30505b0876..2e68a1c939 100644
--- a/crates/collab_ui/src/collab_panel.rs
+++ b/crates/collab_ui/src/collab_panel.rs
@@ -11,7 +11,10 @@ use anyhow::Result;
use call::ActiveCall;
use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
use channel_modal::ChannelModal;
-use client::{proto::PeerId, Client, Contact, User, UserStore};
+use client::{
+ proto::{self, PeerId},
+ Client, Contact, User, UserStore,
+};
use contact_finder::ContactFinder;
use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE;
@@ -428,7 +431,7 @@ enum ListEntry {
is_last: bool,
},
ParticipantScreen {
- peer_id: PeerId,
+ peer_id: Option,
is_last: bool,
},
IncomingRequest(Arc),
@@ -442,6 +445,9 @@ enum ListEntry {
ChannelNotes {
channel_id: ChannelId,
},
+ ChannelChat {
+ channel_id: ChannelId,
+ },
ChannelEditor {
depth: usize,
},
@@ -602,6 +608,13 @@ impl CollabPanel {
ix,
cx,
),
+ ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
+ *channel_id,
+ &theme.collab_panel,
+ is_selected,
+ ix,
+ cx,
+ ),
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
channel.clone(),
this.channel_store.clone(),
@@ -804,7 +817,8 @@ impl CollabPanel {
let room = room.read(cx);
if let Some(channel_id) = room.channel_id() {
- self.entries.push(ListEntry::ChannelNotes { channel_id })
+ self.entries.push(ListEntry::ChannelNotes { channel_id });
+ self.entries.push(ListEntry::ChannelChat { channel_id })
}
// Populate the active user.
@@ -836,7 +850,13 @@ impl CollabPanel {
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
host_user_id: user_id,
- is_last: projects.peek().is_none(),
+ is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+ });
+ }
+ if room.is_screen_sharing() {
+ self.entries.push(ListEntry::ParticipantScreen {
+ peer_id: None,
+ is_last: true,
});
}
}
@@ -880,7 +900,7 @@ impl CollabPanel {
}
if !participant.video_tracks.is_empty() {
self.entries.push(ListEntry::ParticipantScreen {
- peer_id: participant.peer_id,
+ peer_id: Some(participant.peer_id),
is_last: true,
});
}
@@ -1225,14 +1245,18 @@ impl CollabPanel {
) -> AnyElement {
enum CallParticipant {}
enum CallParticipantTooltip {}
+ enum LeaveCallButton {}
+ enum LeaveCallTooltip {}
let collab_theme = &theme.collab_panel;
let is_current_user =
user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
- let content =
- MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| {
+ let content = MouseEventHandler::new::(
+ user.id as usize,
+ cx,
+ |mouse_state, cx| {
let style = if is_current_user {
*collab_theme
.contact_row
@@ -1268,14 +1292,32 @@ impl CollabPanel {
Label::new("Calling", collab_theme.calling_indicator.text.clone())
.contained()
.with_style(collab_theme.calling_indicator.container)
- .aligned(),
+ .aligned()
+ .into_any(),
)
} else if is_current_user {
Some(
- Label::new("You", collab_theme.calling_indicator.text.clone())
- .contained()
- .with_style(collab_theme.calling_indicator.container)
- .aligned(),
+ MouseEventHandler::new::(0, cx, |state, _| {
+ render_icon_button(
+ theme
+ .collab_panel
+ .leave_call_button
+ .style_for(is_selected, state),
+ "icons/exit.svg",
+ )
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, |_, _, cx| {
+ Self::leave_call(cx);
+ })
+ .with_tooltip::(
+ 0,
+ "Leave call",
+ None,
+ theme.tooltip.clone(),
+ cx,
+ )
+ .into_any(),
)
} else {
None
@@ -1284,7 +1326,8 @@ impl CollabPanel {
.with_height(collab_theme.row_height)
.contained()
.with_style(style)
- });
+ },
+ );
if is_current_user || is_pending || peer_id.is_none() {
return content.into_any();
@@ -1406,7 +1449,7 @@ impl CollabPanel {
}
fn render_participant_screen(
- peer_id: PeerId,
+ peer_id: Option,
is_last: bool,
is_selected: bool,
theme: &theme::CollabPanel,
@@ -1421,8 +1464,8 @@ impl CollabPanel {
.unwrap_or(0.);
let tree_branch = theme.tree_branch;
- MouseEventHandler::new::(
- peer_id.as_u64() as usize,
+ let handler = MouseEventHandler::new::(
+ peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
cx,
|mouse_state, cx| {
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
@@ -1460,16 +1503,20 @@ impl CollabPanel {
.contained()
.with_style(row.container)
},
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- workspace.open_shared_screen(peer_id, cx)
- });
- }
- })
- .into_any()
+ );
+ if peer_id.is_none() {
+ return handler.into_any();
+ }
+ handler
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ workspace.update(cx, |workspace, cx| {
+ workspace.open_shared_screen(peer_id.unwrap(), cx)
+ });
+ }
+ })
+ .into_any()
}
fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool {
@@ -1496,23 +1543,32 @@ impl CollabPanel {
enum AddChannel {}
let tooltip_style = &theme.tooltip;
+ let mut channel_link = None;
+ let mut channel_tooltip_text = None;
+ let mut channel_icon = None;
+
let text = match section {
Section::ActiveCall => {
let channel_name = iife!({
let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
- let name = self
- .channel_store
- .read(cx)
- .channel_for_id(channel_id)?
- .name
- .as_str();
+ let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
- Some(name)
+ 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."))
+ }
+ proto::ChannelVisibility::Members => {
+ (Some("icons/hash.svg"), Some("Copy private channel link."))
+ }
+ };
+
+ Some(channel.name.as_str())
});
if let Some(name) = channel_name {
- Cow::Owned(format!("#{}", name))
+ Cow::Owned(format!("{}", name))
} else {
Cow::Borrowed("Current Call")
}
@@ -1527,28 +1583,30 @@ impl CollabPanel {
enum AddContact {}
let button = match section {
- Section::ActiveCall => Some(
+ Section::ActiveCall => channel_link.map(|channel_link| {
+ let channel_link_copy = channel_link.clone();
MouseEventHandler::new::(0, cx, |state, _| {
render_icon_button(
theme
.collab_panel
.leave_call_button
.style_for(is_selected, state),
- "icons/exit.svg",
+ "icons/link.svg",
)
})
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, |_, _, cx| {
- Self::leave_call(cx);
+ .on_click(MouseButton::Left, move |_, _, cx| {
+ let item = ClipboardItem::new(channel_link_copy.clone());
+ cx.write_to_clipboard(item)
})
.with_tooltip::(
0,
- "Leave call",
+ channel_tooltip_text.unwrap(),
None,
tooltip_style.clone(),
cx,
- ),
- ),
+ )
+ }),
Section::Contacts => Some(
MouseEventHandler::new::(0, cx, |state, _| {
render_icon_button(
@@ -1633,6 +1691,21 @@ impl CollabPanel {
theme.collab_panel.contact_username.container.margin.left,
),
)
+ } else if let Some(channel_icon) = channel_icon {
+ Some(
+ Svg::new(channel_icon)
+ .with_color(header_style.text.color)
+ .constrained()
+ .with_max_width(icon_size)
+ .with_max_height(icon_size)
+ .aligned()
+ .constrained()
+ .with_width(icon_size)
+ .contained()
+ .with_margin_right(
+ theme.collab_panel.contact_username.container.margin.left,
+ ),
+ )
} else {
None
})
@@ -1908,6 +1981,12 @@ impl CollabPanel {
let channel_id = channel.id;
let collab_theme = &theme.collab_panel;
let has_children = self.channel_store.read(cx).has_children(channel_id);
+ let is_public = self
+ .channel_store
+ .read(cx)
+ .channel_for_id(channel_id)
+ .map(|channel| channel.visibility)
+ == Some(proto::ChannelVisibility::Public);
let other_selected =
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
@@ -1965,12 +2044,16 @@ impl CollabPanel {
Flex::::row()
.with_child(
- Svg::new("icons/hash.svg")
- .with_color(collab_theme.channel_hash.color)
- .constrained()
- .with_width(collab_theme.channel_hash.width)
- .aligned()
- .left(),
+ Svg::new(if is_public {
+ "icons/public.svg"
+ } else {
+ "icons/hash.svg"
+ })
+ .with_color(collab_theme.channel_hash.color)
+ .constrained()
+ .with_width(collab_theme.channel_hash.width)
+ .aligned()
+ .left(),
)
.with_child({
let style = collab_theme.channel_name.inactive_state();
@@ -2275,7 +2358,7 @@ impl CollabPanel {
.with_child(render_tree_branch(
tree_branch,
&row.name.text,
- true,
+ false,
vec2f(host_avatar_width, theme.row_height),
cx.font_cache(),
))
@@ -2308,6 +2391,62 @@ impl CollabPanel {
.into_any()
}
+ fn render_channel_chat(
+ &self,
+ channel_id: ChannelId,
+ theme: &theme::CollabPanel,
+ is_selected: bool,
+ ix: usize,
+ cx: &mut ViewContext,
+ ) -> AnyElement {
+ enum ChannelChat {}
+ let host_avatar_width = theme
+ .contact_avatar
+ .width
+ .or(theme.contact_avatar.height)
+ .unwrap_or(0.);
+
+ MouseEventHandler::new::(ix as usize, cx, |state, cx| {
+ let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+ let row = theme.project_row.in_state(is_selected).style_for(state);
+
+ Flex::::row()
+ .with_child(render_tree_branch(
+ tree_branch,
+ &row.name.text,
+ true,
+ vec2f(host_avatar_width, theme.row_height),
+ cx.font_cache(),
+ ))
+ .with_child(
+ Svg::new("icons/conversations.svg")
+ .with_color(theme.channel_hash.color)
+ .constrained()
+ .with_width(theme.channel_hash.width)
+ .aligned()
+ .left(),
+ )
+ .with_child(
+ Label::new("chat", theme.channel_name.text.clone())
+ .contained()
+ .with_style(theme.channel_name.container)
+ .aligned()
+ .left()
+ .flex(1., true),
+ )
+ .constrained()
+ .with_height(theme.row_height)
+ .contained()
+ .with_style(*theme.channel_row.style_for(is_selected, state))
+ .with_padding_left(theme.channel_row.default_style().padding.left)
+ })
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .into_any()
+ }
+
fn render_channel_invite(
channel: Arc,
channel_store: ModelHandle,
@@ -2771,6 +2910,9 @@ impl CollabPanel {
}
}
ListEntry::ParticipantScreen { peer_id, .. } => {
+ let Some(peer_id) = peer_id else {
+ return;
+ };
if let Some(workspace) = self.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(*peer_id, cx)
@@ -3498,6 +3640,14 @@ impl PartialEq for ListEntry {
return channel_id == other_id;
}
}
+ ListEntry::ChannelChat { channel_id } => {
+ if let ListEntry::ChannelChat {
+ channel_id: other_id,
+ } = other
+ {
+ return channel_id == other_id;
+ }
+ }
ListEntry::ChannelInvite(channel_1) => {
if let ListEntry::ChannelInvite(channel_2) = other {
return channel_1.id == channel_2.id;