Merge branch 'main' into notifications

This commit is contained in:
Max Brunsfeld 2023-10-18 17:20:04 -07:00
commit b07f9fe3b5
61 changed files with 3185 additions and 1148 deletions

View file

@ -345,8 +345,12 @@ impl ChatPanel {
}
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let (message, is_continuation, is_last) = {
let (message, is_continuation, is_last, is_admin) = {
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
let is_admin = self
.channel_store
.read(cx)
.is_user_admin(active_chat.channel().id);
let last_message = active_chat.message(ix.saturating_sub(1));
let this_message = active_chat.message(ix);
let is_continuation = last_message.id != this_message.id
@ -356,6 +360,7 @@ impl ChatPanel {
active_chat.message(ix).clone(),
is_continuation,
active_chat.message_count() == ix + 1,
is_admin,
)
};
@ -376,12 +381,13 @@ impl ChatPanel {
};
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
let message_id_to_remove =
if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) {
Some(id)
} else {
None
};
let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
(message.id, belongs_to_user || is_admin)
{
Some(id)
} else {
None
};
enum MessageBackgroundHighlight {}
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {

View file

@ -232,7 +232,7 @@ mod tests {
avatar: None,
}),
kind: proto::channel_member::Kind::Member,
admin: false,
role: proto::ChannelRole::Member,
},
ChannelMembership {
user: Arc::new(User {
@ -241,7 +241,7 @@ mod tests {
avatar: None,
}),
kind: proto::channel_member::Kind::Member,
admin: false,
role: proto::ChannelRole::Member,
},
],
cx,

View file

@ -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<PeerId>,
is_last: bool,
},
IncomingRequest(Arc<User>),
@ -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<Self> {
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::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
let content = MouseEventHandler::new::<CallParticipant, _>(
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::<LeaveCallButton, _>(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::<LeaveCallTooltip>(
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<PeerId>,
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::<OpenSharedScreen, _>(
peer_id.as_u64() as usize,
let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
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<Self>) -> 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::<AddContact, _>(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::<AddContact>(
0,
"Leave call",
channel_tooltip_text.unwrap(),
None,
tooltip_style.clone(),
cx,
),
),
)
}),
Section::Contacts => Some(
MouseEventHandler::new::<LeaveCallContactList, _>(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::<Self>::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<Self>,
) -> AnyElement<Self> {
enum ChannelChat {}
let host_avatar_width = theme
.contact_avatar
.width
.or(theme.contact_avatar.height)
.unwrap_or(0.);
MouseEventHandler::new::<ChannelChat, _>(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::<Self>::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>,
channel_store: ModelHandle<ChannelStore>,
@ -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)
@ -3499,6 +3641,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;

View file

@ -1,12 +1,16 @@
use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::{proto, User, UserId, UserStore};
use client::{
proto::{self, ChannelRole, ChannelVisibility},
User, UserId, UserStore,
};
use context_menu::{ContextMenu, ContextMenuItem};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions,
elements::*,
platform::{CursorStyle, MouseButton},
AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc;
@ -96,11 +100,14 @@ impl ChannelModal {
let channel_id = self.channel_id;
cx.spawn(|this, mut cx| async move {
if mode == Mode::ManageMembers {
let members = channel_store
let mut members = channel_store
.update(&mut cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
})
.await?;
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
this.update(&mut cx, |this, cx| {
this.picker
.update(cx, |picker, _| picker.delegate_mut().members = members);
@ -182,6 +189,81 @@ impl View for ChannelModal {
.into_any()
}
fn render_visibility(
channel_id: ChannelId,
visibility: ChannelVisibility,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
enum TogglePublic {}
if visibility == ChannelVisibility::Members {
return Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Public,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any();
}
Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: ON"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Members,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.with_spacing(14.0)
.with_child(
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
let style = theme.channel_link.style_for(state);
Label::new(format!("{}", "copy link"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(channel) =
this.channel_store.read(cx).channel_for_id(channel_id)
{
let item = ClipboardItem::new(channel.link());
cx.write_to_clipboard(item);
}
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any()
}
Flex::column()
.with_child(
Flex::column()
@ -190,6 +272,7 @@ impl View for ChannelModal {
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
.with_child(Flex::row().with_children([
render_mode_button::<InviteMembers>(
Mode::InviteMembers,
@ -343,9 +426,11 @@ impl PickerDelegate for ChannelModalDelegate {
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
match self.mode {
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
Mode::ManageMembers => {
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
}
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
Some(proto::channel_member::Kind::Invitee) => {
self.remove_selected_member(cx);
@ -373,7 +458,7 @@ impl PickerDelegate for ChannelModalDelegate {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.channel_modal;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let (user, admin) = self.user_at_index(ix).unwrap();
let (user, role) = self.user_at_index(ix).unwrap();
let request_status = self.member_status(user.id, cx);
let style = tabbed_modal
@ -409,15 +494,25 @@ impl PickerDelegate for ChannelModalDelegate {
},
)
})
.with_children(admin.and_then(|admin| {
(in_manage && admin).then(|| {
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
Some(
Label::new("Admin", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left()
})
}))
.left(),
)
} else if in_manage && role == Some(ChannelRole::Guest) {
Some(
Label::new("Guest", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left(),
)
} else {
None
})
.with_children({
let svg = match self.mode {
Mode::ManageMembers => Some(
@ -502,13 +597,13 @@ impl ChannelModalDelegate {
})
}
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
let channel_membership = self.members.get(*ix)?;
Some((
channel_membership.user.clone(),
Some(channel_membership.admin),
Some(channel_membership.role),
))
}),
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
@ -516,17 +611,21 @@ impl ChannelModalDelegate {
}
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, admin) = self.user_at_index(self.selected_index)?;
let admin = !admin.unwrap_or(false);
let (user, role) = self.user_at_index(self.selected_index)?;
let new_role = if role == Some(ChannelRole::Admin) {
ChannelRole::Member
} else {
ChannelRole::Admin
};
let update = self.channel_store.update(cx, |store, cx| {
store.set_member_admin(self.channel_id, user.id, admin, cx)
store.set_member_role(self.channel_id, user.id, new_role, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
member.admin = admin;
member.role = new_role;
}
cx.focus_self();
cx.notify();
@ -572,25 +671,30 @@ impl ChannelModalDelegate {
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
let invite_member = self.channel_store.update(cx, |store, cx| {
store.invite_member(self.channel_id, user.id, false, cx)
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
});
cx.spawn(|this, mut cx| async move {
invite_member.await?;
this.update(&mut cx, |this, cx| {
this.delegate_mut().members.push(ChannelMembership {
let new_member = ChannelMembership {
user,
kind: proto::channel_member::Kind::Invitee,
admin: false,
});
role: ChannelRole::Member,
};
let members = &mut this.delegate_mut().members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
cx.notify();
})
})
.detach_and_log_err(cx);
}
fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
self.context_menu.update(cx, |context_menu, cx| {
context_menu.show(
Default::default(),
@ -598,7 +702,7 @@ impl ChannelModalDelegate {
vec![
ContextMenuItem::action("Remove", RemoveMember),
ContextMenuItem::action(
if user_is_admin {
if role == ChannelRole::Admin {
"Make non-admin"
} else {
"Make admin"